add sborka
This commit is contained in:
199
garrysmod/gamemodes/militaryrp/plugins/admin_magaz/cl_plugin.lua
Normal file
199
garrysmod/gamemodes/militaryrp/plugins/admin_magaz/cl_plugin.lua
Normal file
@@ -0,0 +1,199 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
surface.CreateFont("ixAdminShopTitle", { font = "Montserrat", size = 28, weight = 900, antialias = true })
|
||||
surface.CreateFont("ixAdminShopTab", { font = "Montserrat", size = 18, weight = 700, antialias = true })
|
||||
surface.CreateFont("ixAdminShopItem", { font = "Montserrat", size = 20, weight = 800, antialias = true })
|
||||
surface.CreateFont("ixAdminShopDesc", { font = "Montserrat", size = 14, weight = 500, antialias = true })
|
||||
surface.CreateFont("ixAdminShopPrice", { font = "Montserrat", size = 18, weight = 900, antialias = true })
|
||||
surface.CreateFont("ixAdminShopSmall", { font = "Montserrat", size = 14, weight = 600, antialias = true })
|
||||
surface.CreateFont("ixAdminShopBold", { font = "Montserrat", size = 16, weight = 800, antialias = true })
|
||||
|
||||
-- Modern Color Palette #00431c
|
||||
local COLOR_BASE = Color(0, 67, 28)
|
||||
local COLOR_BG = Color(15, 18, 16, 250)
|
||||
local COLOR_CARD = Color(22, 26, 24, 255)
|
||||
local COLOR_CARD_LIGHT = Color(30, 36, 32, 255)
|
||||
local COLOR_TEXT = Color(240, 245, 240)
|
||||
local COLOR_TEXT_DIM = Color(140, 150, 145)
|
||||
local COLOR_ACCENT = Color(0, 140, 60)
|
||||
local COLOR_DANGER = Color(200, 50, 50)
|
||||
local COLOR_WARN = Color(200, 150, 0)
|
||||
|
||||
function PLUGIN:WrapText(text, width, font)
|
||||
surface.SetFont(font)
|
||||
local words = string.Explode(" ", text)
|
||||
local lines = {}
|
||||
local currentLine = ""
|
||||
|
||||
for _, word in ipairs(words) do
|
||||
local testLine = currentLine == "" and word or currentLine .. " " .. word
|
||||
local w, _ = surface.GetTextSize(testLine)
|
||||
|
||||
if w > width then
|
||||
table.insert(lines, currentLine)
|
||||
currentLine = word
|
||||
else
|
||||
currentLine = testLine
|
||||
end
|
||||
end
|
||||
|
||||
if currentLine != "" then
|
||||
table.insert(lines, currentLine)
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
local PANEL = {}
|
||||
|
||||
function PANEL:Init()
|
||||
self:SetSize(ScrW() * 0.7, ScrH() * 0.75)
|
||||
self:Center()
|
||||
self:MakePopup()
|
||||
self:SetTitle("")
|
||||
self:ShowCloseButton(false)
|
||||
self.currentView = "shop"
|
||||
|
||||
self:SetAlpha(0)
|
||||
self:AlphaTo(255, 0.2, 0)
|
||||
|
||||
self.startTime = SysTime()
|
||||
|
||||
self.Paint = function(s, w, h)
|
||||
Derma_DrawBackgroundBlur(s, s.startTime)
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG)
|
||||
surface.SetDrawColor(COLOR_BASE.r, COLOR_BASE.g, COLOR_BASE.b, 150)
|
||||
surface.SetMaterial(Material("vgui/gradient-u"))
|
||||
surface.DrawTexturedRect(0, 0, w, 80)
|
||||
surface.SetDrawColor(COLOR_BASE)
|
||||
surface.DrawRect(0, 80, w, 2)
|
||||
|
||||
local title = self.currentView == "shop" and "АДМИН МАГАЗИН" or (self.currentView == "inventory" and "МОЙ ИНВЕНТАРЬ" or "УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ")
|
||||
draw.SimpleText(title, "ixAdminShopTitle", 40, 40, COLOR_TEXT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
|
||||
local points = LocalPlayer():GetAdminPoints()
|
||||
draw.SimpleText("Текущий баланс:", "ixAdminShopSmall", w * 0.45, 40, COLOR_TEXT_DIM, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText(points .. " Очков", "ixAdminShopItem", w * 0.45 + 10, 40, COLOR_ACCENT, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
local closeBtn = self:Add("DButton")
|
||||
closeBtn:SetSize(40, 40)
|
||||
closeBtn:SetPos(self:GetWide() - 50, 20)
|
||||
closeBtn:SetText("✕")
|
||||
closeBtn:SetFont("ixAdminShopTitle")
|
||||
closeBtn:SetTextColor(COLOR_TEXT_DIM)
|
||||
closeBtn.Paint = nil
|
||||
closeBtn.DoClick = function()
|
||||
self:AlphaTo(0, 0.2, 0, function() self:Close() end)
|
||||
end
|
||||
closeBtn.OnCursorEntered = function(s) s:SetTextColor(COLOR_DANGER) end
|
||||
closeBtn.OnCursorExited = function(s) s:SetTextColor(COLOR_TEXT_DIM) end
|
||||
|
||||
self.nav = self:Add("Panel")
|
||||
self.nav:SetSize(450, 40)
|
||||
self.nav:SetPos(self:GetWide() - 520, 20)
|
||||
|
||||
local function CreateNavBtn(text, view, x, activeColor)
|
||||
local btn = self.nav:Add("DButton")
|
||||
btn:SetSize(140, 40)
|
||||
btn:SetPos(x, 0)
|
||||
btn:SetText(text)
|
||||
btn:SetFont("ixAdminShopBold")
|
||||
btn:SetTextColor(COLOR_TEXT)
|
||||
btn.Paint = function(s, w, h)
|
||||
if self.currentView == view then
|
||||
draw.RoundedBox(4, 0, 0, w, h, activeColor)
|
||||
elseif s:IsHovered() then
|
||||
draw.RoundedBox(4, 0, 0, w, h, Color(255, 255, 255, 10))
|
||||
end
|
||||
end
|
||||
btn.DoClick = function()
|
||||
surface.PlaySound("ui/buttonclick.wav")
|
||||
self.currentView = view
|
||||
self:Refresh()
|
||||
end
|
||||
return btn
|
||||
end
|
||||
|
||||
CreateNavBtn("Магазин", "shop", 0, COLOR_BASE)
|
||||
|
||||
self.content = self:Add("DScrollPanel")
|
||||
self.content:Dock(FILL)
|
||||
self.content:DockMargin(40, 100, 40, 40)
|
||||
|
||||
local sbar = self.content:GetVBar()
|
||||
sbar:SetWide(8)
|
||||
sbar:SetHideButtons(true)
|
||||
sbar.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, Color(0, 0, 0, 100))
|
||||
end
|
||||
sbar.btnGrip.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 2, 0, w-4, h, s:IsHovered() and COLOR_ACCENT or COLOR_BASE)
|
||||
end
|
||||
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function PANEL:Refresh()
|
||||
self.content:Clear()
|
||||
|
||||
if (self.currentView == "shop") then
|
||||
local layout = self.content:Add("DIconLayout")
|
||||
layout:Dock(TOP)
|
||||
layout:SetSpaceX(20)
|
||||
layout:SetSpaceY(20)
|
||||
|
||||
for id, item in pairs(PLUGIN.Items) do
|
||||
local card = layout:Add("DPanel")
|
||||
card:SetSize(250, 340)
|
||||
card.Paint = function(s, w, h)
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_CARD)
|
||||
surface.SetDrawColor(COLOR_CARD_LIGHT)
|
||||
surface.SetMaterial(Material("vgui/gradient-d"))
|
||||
surface.DrawTexturedRect(0, 0, w, 140)
|
||||
|
||||
draw.SimpleText(item.name, "ixAdminShopItem", w/2, 160, COLOR_TEXT, TEXT_ALIGN_CENTER)
|
||||
|
||||
local descLines = PLUGIN:WrapText(item.desc, w - 30, "ixAdminShopDesc")
|
||||
for i, line in ipairs(descLines) do
|
||||
if (i > 4) then break end
|
||||
draw.SimpleText(line, "ixAdminShopDesc", w/2, 185 + (i-1)*18, COLOR_TEXT_DIM, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
end
|
||||
|
||||
local icon = card:Add("DPanel")
|
||||
icon:SetSize(60, 60)
|
||||
icon:SetPos(250/2 - 30, 40)
|
||||
icon.Paint = function(s, w, h)
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG)
|
||||
surface.SetDrawColor(item.color or COLOR_ACCENT)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
end
|
||||
|
||||
local buy = card:Add("DButton")
|
||||
buy:SetSize(210, 45)
|
||||
buy:SetPos(20, 275)
|
||||
buy:SetText("Купить за " .. item.price .. " Очков")
|
||||
buy:SetFont("ixAdminShopBold")
|
||||
buy:SetTextColor(COLOR_TEXT)
|
||||
buy.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, s:IsHovered() and COLOR_ACCENT or COLOR_BASE)
|
||||
end
|
||||
buy.DoClick = function()
|
||||
surface.PlaySound("ui/buttonclick.wav")
|
||||
net.Start("ixAdminShopBuy")
|
||||
net.WriteString(id)
|
||||
net.SendToServer()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
vgui.Register("ixAdminShop", PANEL, "DFrame")
|
||||
|
||||
net.Receive("ixAdminShopOpen", function()
|
||||
if (IsValid(ixAdminShop)) then
|
||||
ixAdminShop:Close()
|
||||
end
|
||||
ixAdminShop = vgui.Create("ixAdminShop")
|
||||
end)
|
||||
@@ -0,0 +1,75 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.name = "Admin Shop"
|
||||
PLUGIN.author = "Scripty"
|
||||
PLUGIN.description = "Adds an admin shop with a unique currency 'Points', inventory, and admin panel."
|
||||
|
||||
PLUGIN.Items = {
|
||||
["remove_warn"] = {
|
||||
name = "Снятие выговора",
|
||||
desc = "Снимает один активный выговор с вашего аккаунта. Выговор будет удален из базы данных, и это отобразится в истории.",
|
||||
price = 150,
|
||||
category = "Other",
|
||||
icon = "icon16/error_delete.png",
|
||||
oneTime = true
|
||||
},
|
||||
["tacrp_ex_m4a1"] = {
|
||||
name = "Оружие: HK416",
|
||||
desc = "Мощная штурмовая винтовка HK416. Доступ на 10 дней. Оружие выдается через инвентарь.",
|
||||
price = 50,
|
||||
category = "Weapons",
|
||||
duration = 10,
|
||||
class = "tacrp_ex_m4a1",
|
||||
color = Color(30, 144, 255)
|
||||
},
|
||||
["tacrp_mg4"] = {
|
||||
name = "Оружие: M249",
|
||||
desc = "Тяжелый пулемет M249. Доступ на 15 дней. Оружие выдается через инвентарь.",
|
||||
price = 60,
|
||||
category = "Weapons",
|
||||
duration = 15,
|
||||
class = "tacrp_mg4",
|
||||
color = Color(255, 140, 0)
|
||||
},
|
||||
["tacrp_ex_hecate"] = {
|
||||
name = "Оружие: MSR",
|
||||
desc = "Снайперская винтовка MSR. Доступ на 13 дней. Оружие выдается через инвентарь.",
|
||||
price = 60,
|
||||
category = "Weapons",
|
||||
duration = 13,
|
||||
class = "tacrp_ex_hecate",
|
||||
color = Color(138, 43, 226)
|
||||
},
|
||||
["tacrp_ak_ak12"] = {
|
||||
name = "Оружие: AK-12",
|
||||
desc = "Современная штурмовая винтовка АК-12. Доступ на 5 дней. Оружие выдается через инвентарь.",
|
||||
price = 45,
|
||||
category = "Weapons",
|
||||
duration = 5,
|
||||
class = "tacrp_ak_ak12",
|
||||
color = Color(255, 215, 0)
|
||||
},
|
||||
["tacrp_sd_aac_hb"] = {
|
||||
name = "Оружие: LVO-AC",
|
||||
desc = "Превосходная штурмовая винтовка TFA LVO-AC. Доступ на 15 дней. Оружие выдается через инвентарь.",
|
||||
price = 50,
|
||||
category = "Weapons",
|
||||
duration = 15,
|
||||
class = "tacrp_sd_aac_hb",
|
||||
color = Color(255, 165, 0)
|
||||
}
|
||||
}
|
||||
|
||||
-- Point utility functions
|
||||
local playerMeta = debug.getregistry().Player
|
||||
|
||||
function playerMeta:GetAdminPoints()
|
||||
return self:GetNetVar("adminPoints", 0)
|
||||
end
|
||||
|
||||
function playerMeta:GetAdminInventory()
|
||||
return self:GetNetVar("adminInventory", {})
|
||||
end
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
184
garrysmod/gamemodes/militaryrp/plugins/admin_magaz/sv_plugin.lua
Normal file
184
garrysmod/gamemodes/militaryrp/plugins/admin_magaz/sv_plugin.lua
Normal file
@@ -0,0 +1,184 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
util.AddNetworkString("ixAdminShopOpen")
|
||||
util.AddNetworkString("ixAdminShopBuy")
|
||||
util.AddNetworkString("ixAdminShopRetrieve")
|
||||
util.AddNetworkString("ixAdminShopAdminSync")
|
||||
util.AddNetworkString("ixAdminShopAdminAction")
|
||||
|
||||
function PLUGIN:Initialize()
|
||||
self.shopData = ix.data.Get("adminShop", {})
|
||||
end
|
||||
|
||||
function PLUGIN:SaveShopData()
|
||||
ix.data.Set("adminShop", self.shopData)
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerLoaded(client)
|
||||
local steamID = client:SteamID()
|
||||
self.shopData[steamID] = self.shopData[steamID] or {points = 0, inventory = {}}
|
||||
|
||||
client:SetNetVar("adminPoints", self.shopData[steamID].points)
|
||||
client:SetNetVar("adminInventory", self.shopData[steamID].inventory)
|
||||
end
|
||||
|
||||
local playerMeta = debug.getregistry().Player
|
||||
|
||||
function playerMeta:SetAdminPoints(amount)
|
||||
amount = math.max(0, amount)
|
||||
self:SetNetVar("adminPoints", amount)
|
||||
|
||||
local steamID = self:SteamID()
|
||||
PLUGIN.shopData[steamID] = PLUGIN.shopData[steamID] or {points = 0, inventory = {}}
|
||||
PLUGIN.shopData[steamID].points = amount
|
||||
PLUGIN:SaveShopData()
|
||||
end
|
||||
|
||||
function playerMeta:GiveAdminPoints(amount)
|
||||
self:SetAdminPoints(self:GetAdminPoints() + amount)
|
||||
end
|
||||
|
||||
function playerMeta:TakeAdminPoints(amount)
|
||||
self:SetAdminPoints(self:GetAdminPoints() - amount)
|
||||
end
|
||||
|
||||
function playerMeta:SetAdminInventory(data)
|
||||
self:SetNetVar("adminInventory", data)
|
||||
|
||||
local steamID = self:SteamID()
|
||||
PLUGIN.shopData[steamID] = PLUGIN.shopData[steamID] or {points = 0, inventory = {}}
|
||||
PLUGIN.shopData[steamID].inventory = data
|
||||
PLUGIN:SaveShopData()
|
||||
end
|
||||
|
||||
ix.command.Add("AdminShop", {
|
||||
description = "Открыть магазин администратора.",
|
||||
OnRun = function(self, client)
|
||||
if (!client:IsAdmin() and !client:IsSuperAdmin() and !IsAdminRank(client:GetUserGroup())) then
|
||||
client:Notify("Эта команда доступна только администраторам.")
|
||||
return
|
||||
end
|
||||
net.Start("ixAdminShopOpen")
|
||||
net.Send(client)
|
||||
end
|
||||
})
|
||||
|
||||
net.Receive("ixAdminShopBuy", function(len, client)
|
||||
local itemID = net.ReadString()
|
||||
local item = PLUGIN.Items[itemID]
|
||||
|
||||
if (!item) then return end
|
||||
|
||||
local points = client:GetAdminPoints()
|
||||
if (points < item.price) then
|
||||
client:Notify("У вас недостаточно очков!")
|
||||
return
|
||||
end
|
||||
|
||||
if (itemID == "remove_warn") then
|
||||
local warnPlugin = ix.plugin.Get("warn")
|
||||
if (warnPlugin) then
|
||||
local steamID = client:SteamID()
|
||||
local warns = ix.data.Get("warns", {})
|
||||
local targetData = warns[steamID]
|
||||
|
||||
if (targetData and targetData.count > 0) then
|
||||
targetData.count = targetData.count - 1
|
||||
table.insert(targetData.history, {
|
||||
admin = "Admin Shop",
|
||||
adminSteamID = "STEAM_0:0:0",
|
||||
reason = "Покупка в магазине",
|
||||
time = os.time(),
|
||||
type = "remove"
|
||||
})
|
||||
ix.data.Set("warns", warns)
|
||||
warnPlugin.warns = warns
|
||||
client:TakeAdminPoints(item.price)
|
||||
client:Notify("Вы успешно сняли выговор!")
|
||||
else
|
||||
client:Notify("У вас нет активных выговоров для снятия!")
|
||||
end
|
||||
else
|
||||
client:Notify("Система выговоров не найдена!")
|
||||
end
|
||||
else
|
||||
-- Immediate delivery if class exists
|
||||
if (item.class) then
|
||||
client:GiveItem(item.class)
|
||||
client:TakeAdminPoints(item.price)
|
||||
client:Notify("Вы купили " .. item.name .. ". Предмет выдан!")
|
||||
else
|
||||
-- Fallback to inventory just in case (hidden)
|
||||
local inv = client:GetAdminInventory()
|
||||
local expireTime = 0
|
||||
if (item.duration) then
|
||||
expireTime = os.time() + (item.duration * 24 * 60 * 60)
|
||||
end
|
||||
|
||||
table.insert(inv, {
|
||||
id = itemID,
|
||||
buyTime = os.time(),
|
||||
expireTime = expireTime,
|
||||
name = item.name,
|
||||
class = item.class
|
||||
})
|
||||
|
||||
client:SetAdminInventory(inv)
|
||||
client:TakeAdminPoints(item.price)
|
||||
client:Notify("Вы купили " .. item.name .. ". Предмет добавлен в инвентарь (скрыто).")
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
net.Receive("ixAdminShopRetrieve", function(len, client)
|
||||
local index = net.ReadUInt(16)
|
||||
local inv = client:GetAdminInventory()
|
||||
local itemData = inv[index]
|
||||
|
||||
if (!itemData) then return end
|
||||
|
||||
if (itemData.expireTime > 0 and os.time() > itemData.expireTime) then
|
||||
client:Notify("Срок действия предмета истек!")
|
||||
table.remove(inv, index)
|
||||
client:SetAdminInventory(inv)
|
||||
return
|
||||
end
|
||||
|
||||
if (itemData.class) then
|
||||
client:GiveItem(itemData.class)
|
||||
client:Notify("Вы получили: " .. itemData.name)
|
||||
end
|
||||
end)
|
||||
|
||||
local adminRanks = {
|
||||
["super admin"] = true,
|
||||
["superadmin"] = true,
|
||||
["projectteam"] = true,
|
||||
["teh.admin"] = true,
|
||||
["curator"] = true,
|
||||
["sudo-curator"] = true,
|
||||
["asist-sudo"] = true,
|
||||
["admin"] = true,
|
||||
["st.admin"] = true,
|
||||
["ivent"] = true,
|
||||
["st.event"] = true,
|
||||
["event"] = true,
|
||||
["disp"] = true,
|
||||
["assistant"] = true,
|
||||
["prem"] = true,
|
||||
["dsmoder"] = true
|
||||
}
|
||||
|
||||
local function IsAdminRank(rank)
|
||||
if not rank or rank == "user" then return false end
|
||||
|
||||
local lowerRank = string.lower(rank)
|
||||
|
||||
-- Check exact match in the provided list
|
||||
if adminRanks[lowerRank] then return true end
|
||||
|
||||
-- Keep generic string searches just in case
|
||||
if lowerRank == "founder" or lowerRank == "owner" or lowerRank == "manager" then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
194
garrysmod/gamemodes/militaryrp/plugins/adminmode/cl_plugin.lua
Normal file
194
garrysmod/gamemodes/militaryrp/plugins/adminmode/cl_plugin.lua
Normal file
@@ -0,0 +1,194 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
function PLUGIN:HUDPaint()
|
||||
local client = LocalPlayer()
|
||||
if not IsValid(client) then return end
|
||||
|
||||
if not client:IsAdminMode() then return end
|
||||
|
||||
local scrW, scrH = ScrW(), ScrH()
|
||||
local w, h = 220, 36
|
||||
local x, y = scrW / 2 - w / 2, 20
|
||||
|
||||
surface.SetDrawColor(13, 13, 13, 220)
|
||||
surface.DrawRect(x, y, w, h)
|
||||
|
||||
surface.SetDrawColor(200, 80, 80, 255)
|
||||
surface.DrawOutlinedRect(x, y, w, h, 2)
|
||||
|
||||
draw.SimpleText("ADMIN MODE", "ixMenuButtonFont", scrW / 2, y + h / 2,
|
||||
Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
surface.CreateFont("AdminESPFontSmall", {
|
||||
font = "Arial",
|
||||
size = 12,
|
||||
weight = 500
|
||||
})
|
||||
|
||||
CLIENT_ADMIN_GROUPS = CLIENT_ADMIN_GROUPS or {}
|
||||
|
||||
local function Draw3DBox(ent, color)
|
||||
local mins, maxs = ent:OBBMins(), ent:OBBMaxs()
|
||||
local corners = {
|
||||
Vector(mins.x, mins.y, mins.z),
|
||||
Vector(mins.x, maxs.y, mins.z),
|
||||
Vector(maxs.x, maxs.y, mins.z),
|
||||
Vector(maxs.x, mins.y, mins.z),
|
||||
|
||||
Vector(mins.x, mins.y, maxs.z),
|
||||
Vector(mins.x, maxs.y, maxs.z),
|
||||
Vector(maxs.x, maxs.y, maxs.z),
|
||||
Vector(maxs.x, mins.y, maxs.z),
|
||||
}
|
||||
|
||||
local lines = {
|
||||
{1,2},{2,3},{3,4},{4,1},
|
||||
{5,6},{6,7},{7,8},{8,5},
|
||||
{1,5},{2,6},{3,7},{4,8}
|
||||
}
|
||||
|
||||
cam.Start3D()
|
||||
render.SetColorMaterial()
|
||||
for _, l in ipairs(lines) do
|
||||
render.DrawLine(
|
||||
ent:LocalToWorld(corners[l[1]]),
|
||||
ent:LocalToWorld(corners[l[2]]),
|
||||
color, true
|
||||
)
|
||||
end
|
||||
cam.End3D()
|
||||
end
|
||||
|
||||
local function CanSeeESP()
|
||||
local client = LocalPlayer()
|
||||
if not IsValid(client) then return false end
|
||||
if not client:IsAdminMode() then return false end
|
||||
|
||||
local group = client:GetUserGroup()
|
||||
if not CLIENT_ADMIN_GROUPS[group] then return false end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
hook.Add("HUDPaint", "AdminMode_ESP", function()
|
||||
if not CanSeeESP() then return end
|
||||
|
||||
local client = LocalPlayer()
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
if ply ~= client and ply:Alive() then
|
||||
|
||||
local distance = client:GetPos():Distance(ply:GetPos())
|
||||
|
||||
Draw3DBox(ply, Color(255, 60, 60))
|
||||
|
||||
local char = ply:GetCharacter()
|
||||
local top = (ply:GetPos() + Vector(0,0,85)):ToScreen()
|
||||
local bottom = (ply:GetPos() + Vector(0,0,5)):ToScreen()
|
||||
|
||||
local faction = "Неизвестно"
|
||||
local podr = "Неизвестно"
|
||||
local spec = "Неизвестно"
|
||||
local rank = "Неизвестно"
|
||||
|
||||
local samGroup = ply:GetUserGroup()
|
||||
local samName = samGroup
|
||||
|
||||
if sam and sam.ranks and sam.ranks[samGroup] and sam.ranks[samGroup].Name then
|
||||
samName = sam.ranks[samGroup].Name
|
||||
end
|
||||
|
||||
if char then
|
||||
local factionID = char:GetFaction()
|
||||
local factionTable = ix.faction.indices[factionID]
|
||||
|
||||
if factionTable and factionTable.name then
|
||||
faction = factionTable.name
|
||||
end
|
||||
|
||||
if factionTable and factionTable.Podr and char.GetPodr then
|
||||
local id = char:GetPodr()
|
||||
if factionTable.Podr[id] and factionTable.Podr[id].name then
|
||||
podr = factionTable.Podr[id].name
|
||||
end
|
||||
end
|
||||
|
||||
if factionTable and factionTable.Spec and char.GetSpec then
|
||||
local id = char:GetSpec()
|
||||
if factionTable.Spec[id] and factionTable.Spec[id].name then
|
||||
spec = factionTable.Spec[id].name
|
||||
end
|
||||
end
|
||||
|
||||
if factionTable and factionTable.Ranks and char.GetRank then
|
||||
local id = char:GetRank()
|
||||
if factionTable.Ranks[id] and factionTable.Ranks[id][1] then
|
||||
rank = factionTable.Ranks[id][1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local steamid = ply:SteamID()
|
||||
local hp = ply:Health()
|
||||
local armor = ply:Armor()
|
||||
local wep = IsValid(ply:GetActiveWeapon()) and ply:GetActiveWeapon():GetClass() or "—"
|
||||
|
||||
local topLines = {}
|
||||
|
||||
if distance > 500 then
|
||||
-- Издалека: только ник
|
||||
topLines = { ply:Name() }
|
||||
elseif distance > 200 then
|
||||
-- Средняя дальность: ник + фракция + SteamID
|
||||
topLines = {
|
||||
ply:Name(),
|
||||
"" .. faction,
|
||||
"" .. steamid
|
||||
}
|
||||
else
|
||||
-- Близко: вся информация
|
||||
topLines = {
|
||||
ply:Name(),
|
||||
"" .. faction,
|
||||
"" .. podr,
|
||||
"" .. spec,
|
||||
"" .. rank,
|
||||
"" .. samName,
|
||||
"" .. steamid
|
||||
}
|
||||
end
|
||||
|
||||
local y = top.y
|
||||
for _, text in ipairs(topLines) do
|
||||
surface.SetDrawColor(0, 0, 0, 160)
|
||||
surface.DrawRect(top.x - 50, y - 5, 100, 12)
|
||||
|
||||
draw.SimpleText(text, "AdminESPFontSmall", top.x, y,
|
||||
Color(255,255,255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
y = y + 15
|
||||
end
|
||||
|
||||
-- Нижняя информация всегда показывается близко
|
||||
if distance <= 200 then
|
||||
local bottomLines = {
|
||||
"HP: " .. hp,
|
||||
"Armor: " .. armor,
|
||||
"Weapon: " .. wep
|
||||
}
|
||||
|
||||
local y2 = bottom.y
|
||||
for _, text in ipairs(bottomLines) do
|
||||
surface.SetDrawColor(0, 0, 0, 160)
|
||||
surface.DrawRect(bottom.x - 40, y2 - 5, 80, 10)
|
||||
|
||||
draw.SimpleText(text, "AdminESPFontSmall", bottom.x, y2,
|
||||
Color(255,255,255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
y2 = y2 + 12
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
386
garrysmod/gamemodes/militaryrp/plugins/adminmode/sh_plugin.lua
Normal file
386
garrysmod/gamemodes/militaryrp/plugins/adminmode/sh_plugin.lua
Normal file
@@ -0,0 +1,386 @@
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
PLUGIN.name = "Admin Mode"
|
||||
PLUGIN.author = "Scripty"
|
||||
PLUGIN.description = "Перекид игрока по команде в админ фракцию"
|
||||
|
||||
local accessAdminGroups = {
|
||||
["superadmin"] = true,
|
||||
["super admin"] = true,
|
||||
["projectteam"] = true,
|
||||
["teh.admin"] = true,
|
||||
["curator"] = true,
|
||||
["sudo-curator"] = true,
|
||||
["asist-sudo"] = true,
|
||||
["admin"] = true,
|
||||
["st.admin"] = true,
|
||||
["ivent"] = true,
|
||||
["st.event"] = true,
|
||||
["event"] = true,
|
||||
["disp"] = true,
|
||||
["assistant"] = true,
|
||||
["specadmin"] = true
|
||||
}
|
||||
local playerMeta = FindMetaTable("Player")
|
||||
|
||||
function playerMeta:IsAdminMode()
|
||||
return self:GetNetVar("AdminMode", false)
|
||||
end
|
||||
|
||||
local function CanUseAdminMode(ply)
|
||||
return accessAdminGroups[ply:GetUserGroup()] == true
|
||||
end
|
||||
|
||||
local function CanUseNoclip(ply)
|
||||
if ply:IsAdminMode() then return true end
|
||||
|
||||
if ply:GetUserGroup() == "superadmin" then return true end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function GetWeaponsList(ply)
|
||||
local list = {}
|
||||
for _, wep in ipairs(ply:GetWeapons()) do
|
||||
if IsValid(wep) then
|
||||
list[#list + 1] = wep:GetClass()
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
if (SERVER) then
|
||||
util.AddNetworkString("AdminMode_Groups")
|
||||
|
||||
hook.Add("PlayerInitialSpawn", "AdminMode_SendGroups", function(ply)
|
||||
net.Start("AdminMode_Groups")
|
||||
net.WriteTable(accessAdminGroups)
|
||||
net.Send(ply)
|
||||
end)
|
||||
|
||||
function PLUGIN:PlayerDisconnected(ply)
|
||||
if ply:IsAdminMode() then
|
||||
local char = ply:GetCharacter()
|
||||
if char then
|
||||
local oldFaction = char:GetData("AdminMode_OldFaction")
|
||||
if oldFaction then char:SetFaction(oldFaction) end
|
||||
|
||||
local oldPodr = char:GetData("AdminMode_OldPodr")
|
||||
if oldPodr then char:SetPodr(oldPodr) end
|
||||
|
||||
local oldSpec = char:GetData("AdminMode_OldSpec")
|
||||
if oldSpec then char:SetSpec(oldSpec) end
|
||||
|
||||
local oldRank = char:GetData("AdminMode_OldRank")
|
||||
if oldRank then char:SetRank(oldRank) end
|
||||
end
|
||||
ply:SetNetVar("HideFromTab", false)
|
||||
ply:SetNetVar("AdminMode", false)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerLoadedCharacter(ply, char)
|
||||
local squads = ix.plugin.list["squads"]
|
||||
|
||||
if ply:IsAdminMode() and squads then
|
||||
local squad, squadID = squads:GetPlayerSquad(ply)
|
||||
if squad then
|
||||
if squad.leader == ply:SteamID() then
|
||||
squads:DisbandSquad(squadID)
|
||||
else
|
||||
squads:LeaveSquad(ply)
|
||||
end
|
||||
net.Start("ixSquadSync")
|
||||
net.WriteString("{}")
|
||||
net.Send(ply)
|
||||
end
|
||||
end
|
||||
|
||||
if char:GetFaction() == FACTION_ADMIN and not ply:IsAdminMode() then
|
||||
|
||||
local oldFaction = char:GetData("AdminMode_OldFaction")
|
||||
local oldPodr = char:GetData("AdminMode_OldPodr")
|
||||
local oldSpec = char:GetData("AdminMode_OldSpec")
|
||||
local oldRank = char:GetData("AdminMode_OldRank")
|
||||
|
||||
if oldFaction then char:SetFaction(oldFaction) end
|
||||
if oldPodr then char:SetPodr(oldPodr) end
|
||||
if oldSpec then char:SetSpec(oldSpec) end
|
||||
if oldRank then char:SetRank(oldRank) end
|
||||
|
||||
char:SetData("AdminMode_OldFaction", nil)
|
||||
char:SetData("AdminMode_OldPodr", nil)
|
||||
char:SetData("AdminMode_OldSpec", nil)
|
||||
char:SetData("AdminMode_OldRank", nil)
|
||||
char:SetData("AdminMode_OldModel", nil)
|
||||
char:SetData("AdminMode_OldWeapons", nil)
|
||||
|
||||
ply:SetNetVar("AdminMode", false)
|
||||
ply:SetNetVar("HideFromTab", false)
|
||||
|
||||
local radio = ix.plugin.list["radio"]
|
||||
if radio then
|
||||
radio:SyncRadioState(ply, radio.defaultFrequency, false, false)
|
||||
end
|
||||
elseif ply:IsAdminMode() and char:GetFaction() ~= FACTION_ADMIN then
|
||||
self:ToggleAdminMode(ply)
|
||||
end
|
||||
|
||||
if ply:IsAdminMode() then
|
||||
local radio = ix.plugin.list["radio"]
|
||||
if radio then
|
||||
radio:SyncRadioState(ply, radio.defaultFrequency, false, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnCharacterSave(char)
|
||||
local ply = char:GetPlayer()
|
||||
if not IsValid(ply) then return end
|
||||
|
||||
if ply:IsAdminMode() then
|
||||
local oldFaction = char:GetData("AdminMode_OldFaction")
|
||||
if oldFaction then char:SetFaction(oldFaction) end
|
||||
|
||||
local oldPodr = char:GetData("AdminMode_OldPodr")
|
||||
if oldPodr then char:SetPodr(oldPodr) end
|
||||
|
||||
local oldSpec = char:GetData("AdminMode_OldSpec")
|
||||
if oldSpec then char:SetSpec(oldSpec) end
|
||||
|
||||
local oldRank = char:GetData("AdminMode_OldRank")
|
||||
if oldRank then char:SetRank(oldRank) end
|
||||
|
||||
ply:SetNetVar("AdminMode", false)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ToggleAdminMode(ply)
|
||||
if not IsValid(ply) then return end
|
||||
if not CanUseAdminMode(ply) then
|
||||
ply:ChatPrint("[!] У вас нет доступа к админ-моду.")
|
||||
return
|
||||
end
|
||||
|
||||
local char = ply:GetCharacter()
|
||||
if not char then
|
||||
ply:ChatPrint("[!] У вас нет активного персонажа.")
|
||||
return
|
||||
end
|
||||
|
||||
local enable = not ply:IsAdminMode()
|
||||
ply:SetNetVar("AdminMode", enable)
|
||||
|
||||
if enable then
|
||||
local squads = ix.plugin.list["squads"]
|
||||
if squads then
|
||||
local squad, squadID = squads:GetPlayerSquad(ply)
|
||||
if squad then
|
||||
if squad.leader == ply:SteamID() then
|
||||
squads:DisbandSquad(squadID)
|
||||
else
|
||||
squads:LeaveSquad(ply)
|
||||
end
|
||||
end
|
||||
end
|
||||
char:SetData("AdminMode_OldFaction", char:GetFaction())
|
||||
char:SetData("AdminMode_OldPodr", char:GetPodr())
|
||||
char:SetData("AdminMode_OldSpec", char:GetSpec())
|
||||
char:SetData("AdminMode_OldRank", char:GetRank())
|
||||
|
||||
char:SetData("AdminMode_OldModel", ply:GetModel())
|
||||
char:SetData("AdminMode_OldWeapons", GetWeaponsList(ply))
|
||||
|
||||
ply:SetModel("models/ft/ft_admin.mdl")
|
||||
|
||||
if FACTION_ADMIN then
|
||||
char:SetFaction(FACTION_ADMIN)
|
||||
end
|
||||
|
||||
ply:GodEnable()
|
||||
ply:SetNoDraw(true)
|
||||
ply:SetMoveType(MOVETYPE_NOCLIP)
|
||||
|
||||
ply:StripWeapons()
|
||||
ply:Give("weapon_physgun")
|
||||
ply:Give("gmod_tool")
|
||||
ply:Give("ix_hands")
|
||||
ply:SelectWeapon("ix_hands")
|
||||
ply:SetNetVar("HideFromTab", true)
|
||||
|
||||
ply:ChatPrint("[!] Админ-мод включен.")
|
||||
else
|
||||
local oldFaction = char:GetData("AdminMode_OldFaction")
|
||||
local oldPodr = char:GetData("AdminMode_OldPodr")
|
||||
local oldSpec = char:GetData("AdminMode_OldSpec")
|
||||
local oldRank = char:GetData("AdminMode_OldRank")
|
||||
|
||||
if oldFaction and char:GetFaction() == FACTION_ADMIN then char:SetFaction(oldFaction) end
|
||||
if oldPodr then char:SetPodr(oldPodr) end
|
||||
if oldSpec then char:SetSpec(oldSpec) end
|
||||
if oldRank then char:SetRank(oldRank) end
|
||||
|
||||
local oldModel = char:GetData("AdminMode_OldModel")
|
||||
if oldModel then
|
||||
ply:SetModel(oldModel)
|
||||
end
|
||||
|
||||
local oldWeapons = char:GetData("AdminMode_OldWeapons") or {}
|
||||
ply:StripWeapons()
|
||||
|
||||
for _, class in ipairs(oldWeapons) do
|
||||
ply:Give(class)
|
||||
end
|
||||
|
||||
if ply.sam_uncloak then
|
||||
ply:sam_uncloak()
|
||||
end
|
||||
|
||||
ply:SetNoDraw(false)
|
||||
ply:SetRenderMode(RENDERMODE_NORMAL)
|
||||
ply:SetColor(Color(255, 255, 255, 255))
|
||||
ply:Fire("alpha", 255, 0)
|
||||
ply:DrawWorldModel(true)
|
||||
ply:SetMaterial("")
|
||||
|
||||
ply:SetNetVar("ixNoDraw", false)
|
||||
ply:DrawShadow(true)
|
||||
ply:SetupHands()
|
||||
|
||||
if #oldWeapons == 0 then
|
||||
ply:Give("ix_hands")
|
||||
end
|
||||
|
||||
ply:GodDisable()
|
||||
ply:SetMoveType(MOVETYPE_WALK)
|
||||
|
||||
local restoredFaction = char:GetFaction()
|
||||
local class = char:GetClass() or "default"
|
||||
|
||||
local spawnPlugin = ix.plugin.list["spawns"]
|
||||
|
||||
if spawnPlugin and spawnPlugin.spawns then
|
||||
local factionTable = ix.faction.indices[restoredFaction]
|
||||
|
||||
if factionTable then
|
||||
local factionID = factionTable.uniqueID
|
||||
local factionSpawns = spawnPlugin.spawns[factionID]
|
||||
|
||||
if factionSpawns then
|
||||
local className = "default"
|
||||
|
||||
for _, v in ipairs(ix.class.list) do
|
||||
if v.index == class then
|
||||
className = v.uniqueID
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local points = factionSpawns[className] or factionSpawns["default"]
|
||||
|
||||
if points and not table.IsEmpty(points) then
|
||||
local pos = table.Random(points)
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
if IsValid(ply) then
|
||||
ply:SetPos(pos)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
char:SetData("AdminMode_OldFaction", nil)
|
||||
char:SetData("AdminMode_OldPodr", nil)
|
||||
char:SetData("AdminMode_OldSpec", nil)
|
||||
char:SetData("AdminMode_OldRank", nil)
|
||||
char:SetData("AdminMode_OldModel", nil)
|
||||
char:SetData("AdminMode_OldWeapons", nil)
|
||||
ply:SetNetVar("HideFromTab", false)
|
||||
local radio = ix.plugin.list["radio"]
|
||||
if radio then
|
||||
radio:SyncRadioState(ply, radio.defaultFrequency, false, false)
|
||||
end
|
||||
ply:ChatPrint("[!] Админ-мод выключен.")
|
||||
end
|
||||
|
||||
hook.Run("PlayerAdminModeToggled", ply, enable)
|
||||
end
|
||||
|
||||
function PLUGIN:EntityTakeDamage(target, dmg)
|
||||
if IsValid(target) and target:IsPlayer() and target:IsAdminMode() then
|
||||
return true
|
||||
end
|
||||
|
||||
local attacker = dmg:GetAttacker()
|
||||
if IsValid(attacker) and attacker:IsPlayer() and attacker:IsAdminMode() then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnCharacterVarChanged(char, key, oldValue, newValue)
|
||||
if (key == "faction") then
|
||||
local ply = char:GetPlayer()
|
||||
|
||||
if (IsValid(ply) and ply:IsAdminMode() and newValue ~= FACTION_ADMIN) then
|
||||
self:ToggleAdminMode(ply)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerNoClip(ply, desiredState)
|
||||
if not IsValid(ply) then return false end
|
||||
|
||||
if CanUseNoclip(ply) then
|
||||
ply:SetNoDraw(desiredState)
|
||||
|
||||
if desiredState then
|
||||
ply:SetRenderMode(RENDERMODE_NONE)
|
||||
ply:SetColor(Color(255, 255, 255, 0))
|
||||
else
|
||||
ply:SetRenderMode(RENDERMODE_NORMAL)
|
||||
ply:SetColor(Color(255, 255, 255, 255))
|
||||
end
|
||||
|
||||
if desiredState then
|
||||
ply:SetNetVar("HideFromTab", true)
|
||||
ply:SetNetVar("ixNoDraw", true)
|
||||
else
|
||||
if not ply:IsAdminMode() then
|
||||
ply:SetNetVar("HideFromTab", false)
|
||||
ply:SetNetVar("ixNoDraw", false)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
ix.command.Add("admin", {
|
||||
description = "Включить/выключить админ-мод.",
|
||||
OnRun = function(self, ply)
|
||||
PLUGIN:ToggleAdminMode(ply)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("adm", {
|
||||
description = "Синоним /admin.",
|
||||
OnRun = function(self, ply)
|
||||
PLUGIN:ToggleAdminMode(ply)
|
||||
end
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
CLIENT_ADMIN_GROUPS = CLIENT_ADMIN_GROUPS or {}
|
||||
|
||||
if CLIENT then
|
||||
net.Receive("AdminMode_Groups", function()
|
||||
CLIENT_ADMIN_GROUPS = net.ReadTable()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
local PLUGIN = PLUGIN
|
||||
@@ -0,0 +1,5 @@
|
||||
include("shared.lua")
|
||||
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/sw/shared/airdrop_large.mdl")
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetUseType(SIMPLE_USE)
|
||||
|
||||
local phys = self:GetPhysicsObject()
|
||||
if (IsValid(phys)) then
|
||||
phys:Wake()
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Use(activator)
|
||||
if (IsValid(activator) and activator:IsPlayer()) then
|
||||
if (self.opened) then return end
|
||||
self.opened = true
|
||||
|
||||
local weaponClass = self.ixWeapon or "tacrp_m320"
|
||||
local character = activator:GetCharacter()
|
||||
local inventory = character and character:GetInventory()
|
||||
|
||||
if (inventory and !isnumber(inventory) and ix.item.list[weaponClass]) then
|
||||
inventory:Add(weaponClass)
|
||||
ix.util.Notify("Вы получили " .. (ix.item.list[weaponClass].name or weaponClass) .. " из аирдропа.", activator)
|
||||
else
|
||||
activator:Give(weaponClass)
|
||||
ix.util.Notify("Вы получили " .. (self.ixWeapon or "неизвестное оружие") .. " из аирдропа.", activator)
|
||||
end
|
||||
|
||||
if (IsValid(self.ixSmoke)) then
|
||||
self.ixSmoke:Remove()
|
||||
end
|
||||
|
||||
local effect = EffectData()
|
||||
effect:SetOrigin(self:GetPos())
|
||||
effect:SetScale(1)
|
||||
util.Effect("Explosion", effect)
|
||||
|
||||
self:Remove()
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage(damage)
|
||||
if (damage:GetDamage() > 50) then
|
||||
self:Use(damage:GetAttacker())
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
ENT.PrintName = "Airdrop"
|
||||
ENT.Category = "Helix"
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminOnly = true
|
||||
92
garrysmod/gamemodes/militaryrp/plugins/airdrop/sh_plugin.lua
Normal file
92
garrysmod/gamemodes/militaryrp/plugins/airdrop/sh_plugin.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.name = "Airdrop"
|
||||
PLUGIN.author = "Scripty"
|
||||
PLUGIN.description = "Airdrop plugin."
|
||||
|
||||
ix.config.Add("airdropPlaneModel", "models/il76/il_76_fly.mdl", "Модель самолета для аирдропа.", nil, {
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airstrikePlaneModel", "models/gunkov2056/su25.mdl", "Модель самолета для авиаудара.", nil, {
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airdropMinInterval", 3600, "Минимальный интервал между аирдропами (в секундах).", nil, {
|
||||
data = {min = 60, max = 86400},
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airdropMaxInterval", 7200, "Максимальный интервал между аирдропами (в секундах).", nil, {
|
||||
data = {min = 60, max = 86400},
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airdropWeapons", [[tacrp_io_degala
|
||||
tacrp_io_fiveseven
|
||||
tacrp_mr96
|
||||
tacrp_pdw
|
||||
tacrp_superv
|
||||
tacrp_sd_aac_hb
|
||||
tacrp_ak_ak12
|
||||
tacrp_ak_an94
|
||||
tacrp_sg551
|
||||
tacrp_io_xm8car
|
||||
tacrp_hk417
|
||||
tacrp_io_scarh
|
||||
tacrp_mg4
|
||||
tacrp_io_xm8lmg
|
||||
tacrp_io_sg550r
|
||||
tacrp_io_sl8
|
||||
tacrp_io_sg550
|
||||
tacrp_io_vss
|
||||
tacrp_as50
|
||||
tacrp_ex_hecate
|
||||
tacrp_civ_m320]], "Список оружия, которое может выпасть из аирдропа (каждое с новой строки).", nil, {
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airstrikeMinInterval", 1800, "Минимальный интервал между авиаударами (в секундах).", nil, {
|
||||
data = {min = 60, max = 86400},
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.config.Add("airstrikeMaxInterval", 3600, "Максимальный интервал между авиаударами (в секундах).", nil, {
|
||||
data = {min = 60, max = 86400},
|
||||
category = "Airdrop"
|
||||
})
|
||||
|
||||
ix.command.Add("AirdropForce", {
|
||||
description = "Принудительно вызвать случайный аирдроп.",
|
||||
privilege = "Manage Airdrops",
|
||||
superAdminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:SpawnAirdrop()
|
||||
ix.util.Notify("Аирдроп был вызван администратором.", nil, "all")
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("AirdropGive", {
|
||||
description = "Вызвать аирдроп с конкретным оружием.",
|
||||
privilege = "Manage Airdrops",
|
||||
superAdminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, weapon)
|
||||
PLUGIN:SpawnAirdrop(weapon)
|
||||
ix.util.Notify("Аирдроп с " .. weapon .. " был вызван администратором.", nil, "all")
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("AirstrikeForce", {
|
||||
description = "Принудительно вызвать авиаудар.",
|
||||
privilege = "Manage Airdrops",
|
||||
superAdminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:SpawnAirstrike()
|
||||
ix.util.Notify("Авиаудар был вызван администратором.", nil, "all")
|
||||
end
|
||||
})
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
329
garrysmod/gamemodes/militaryrp/plugins/airdrop/sv_plugin.lua
Normal file
329
garrysmod/gamemodes/militaryrp/plugins/airdrop/sv_plugin.lua
Normal file
@@ -0,0 +1,329 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
-- Координаты баз для запрета авиаударов
|
||||
local BASE_RF = {
|
||||
min = Vector(-12686, 4350, -10000),
|
||||
max = Vector(-8714, 12126, 10000)
|
||||
}
|
||||
local BASE_UK = {
|
||||
min = Vector(9778, 2972, -10000),
|
||||
max = Vector(13576, 10852, 10000)
|
||||
}
|
||||
|
||||
local function IsInBase(pos)
|
||||
if (pos:WithinAABox(BASE_RF.min, BASE_RF.max)) then return true end
|
||||
if (pos:WithinAABox(BASE_UK.min, BASE_UK.max)) then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function PLUGIN:SpawnAirdrop(weapon)
|
||||
local center = Vector(0, 0, 0)
|
||||
local players = player.GetAll()
|
||||
if (#players > 0) then
|
||||
local targetPlayer = players[math.random(#players)]
|
||||
center = targetPlayer:GetPos()
|
||||
end
|
||||
|
||||
local angle = math.random(0, 360)
|
||||
local radian = math.rad(angle)
|
||||
local altitude = math.random(2000, 2500)
|
||||
local startPos = center + Vector(math.cos(radian) * 15000, math.sin(radian) * 15000, 0)
|
||||
startPos.z = altitude
|
||||
local endPos = center - Vector(math.cos(radian) * 15000, math.sin(radian) * 15000, 0)
|
||||
endPos.z = altitude
|
||||
|
||||
util.PrecacheModel(ix.config.Get("airdropPlaneModel", "models/gunkov2056/su25.mdl"))
|
||||
local plane = ents.Create("prop_dynamic")
|
||||
plane:SetModel(ix.config.Get("airdropPlaneModel", "models/gunkov2056/su25.mdl"))
|
||||
plane:SetPos(startPos)
|
||||
local angles = (endPos - startPos):Angle()
|
||||
plane:SetAngles(angles)
|
||||
plane:SetMoveType(MOVETYPE_FLY)
|
||||
plane:Spawn()
|
||||
plane:Activate()
|
||||
|
||||
print("[Airdrop] Спавн самолета: " .. plane:GetModel() .. " на " .. tostring(startPos))
|
||||
|
||||
local speed = 2500
|
||||
local distance = startPos:Distance(endPos)
|
||||
local duration = distance / speed
|
||||
local dropFrac = math.Rand(0.45, 0.55)
|
||||
local dropped = false
|
||||
local startTime = CurTime()
|
||||
|
||||
plane:EmitSound("su25/su25.wav", 150, 100, 1, CHAN_AUTO)
|
||||
|
||||
timer.Create("AirdropPlaneMove_" .. plane:EntIndex(), 0.05, 0, function()
|
||||
if (!IsValid(plane)) then return end
|
||||
local elapsed = CurTime() - startTime
|
||||
local frac = elapsed / duration
|
||||
if (frac >= 1) then
|
||||
plane:StopSound("su25/su25.wav")
|
||||
plane:Remove()
|
||||
timer.Remove("AirdropPlaneMove_" .. plane:EntIndex())
|
||||
return
|
||||
end
|
||||
plane:SetPos(LerpVector(frac, startPos, endPos))
|
||||
if (!dropped and frac >= dropFrac) then
|
||||
dropped = true
|
||||
self:DropCrate(plane:GetPos(), weapon)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:SpawnAirstrike()
|
||||
ix.util.Notify("Внимание! Обнаружена воздушная угроза. Авиаудар через 30 секунд!", nil, "all")
|
||||
|
||||
timer.Simple(30, function()
|
||||
local targetPos = Vector(0, 0, 0)
|
||||
local players = player.GetAll()
|
||||
local attempts = 0
|
||||
|
||||
-- Ищем валидную точку для удара (не в базе)
|
||||
repeat
|
||||
if (#players > 0) then
|
||||
targetPos = players[math.random(#players)]:GetPos()
|
||||
else
|
||||
targetPos = Vector(math.random(-10000, 10000), math.random(-10000, 10000), 0)
|
||||
end
|
||||
attempts = attempts + 1
|
||||
until (!IsInBase(targetPos) or attempts > 10)
|
||||
|
||||
if (IsInBase(targetPos)) then return end -- Отмена если не нашли безопасную точку
|
||||
|
||||
local angle = math.random(0, 360)
|
||||
local radian = math.rad(angle)
|
||||
local altitude = math.random(2000, 2500)
|
||||
local startPos = targetPos + Vector(math.cos(radian) * 15000, math.sin(radian) * 15000, 0)
|
||||
startPos.z = altitude
|
||||
local endPos = targetPos - Vector(math.cos(radian) * 15000, math.sin(radian) * 15000, 0)
|
||||
endPos.z = altitude
|
||||
|
||||
util.PrecacheModel(ix.config.Get("airstrikePlaneModel", "models/gunkov2056/su25.mdl"))
|
||||
local plane = ents.Create("prop_dynamic")
|
||||
plane:SetModel(ix.config.Get("airstrikePlaneModel", "models/gunkov2056/su25.mdl"))
|
||||
plane:SetPos(startPos)
|
||||
plane:SetAngles((endPos - startPos):Angle())
|
||||
plane:SetMoveType(MOVETYPE_FLY)
|
||||
plane:Spawn()
|
||||
plane:Activate()
|
||||
|
||||
print("[Airstrike] Спавн самолета: " .. plane:GetModel() .. " на " .. tostring(startPos))
|
||||
|
||||
local speed = 3000
|
||||
local duration = startPos:Distance(endPos) / speed
|
||||
local dropped = false
|
||||
local startTime = CurTime()
|
||||
|
||||
plane:EmitSound("su25/su25.wav", 150, 100, 1, CHAN_AUTO)
|
||||
|
||||
timer.Create("AirstrikePlaneMove_" .. plane:EntIndex(), 0.05, 0, function()
|
||||
if (!IsValid(plane)) then return end
|
||||
local elapsed = CurTime() - startTime
|
||||
local frac = elapsed / duration
|
||||
if (frac >= 1) then
|
||||
plane:StopSound("su25/su25.wav")
|
||||
plane:Remove()
|
||||
timer.Remove("AirstrikePlaneMove_" .. plane:EntIndex())
|
||||
return
|
||||
end
|
||||
plane:SetPos(LerpVector(frac, startPos, endPos))
|
||||
if (!dropped and frac >= 0.5) then
|
||||
dropped = true
|
||||
self:DropBomb(plane:GetPos(), plane:GetForward() * speed)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:DropBomb(pos, velocity)
|
||||
local bomb = ents.Create("sw_bomb_fab250m62_v3")
|
||||
if (!IsValid(bomb)) then
|
||||
-- Если энтити нет, создадим обычный взрыв для теста
|
||||
local exp = ents.Create("env_explosion")
|
||||
exp:SetPos(pos)
|
||||
exp:SetKeyValue("iMagnitude", "300")
|
||||
exp:Spawn()
|
||||
exp:Fire("Explode", 0, 0)
|
||||
return
|
||||
end
|
||||
|
||||
bomb:SetPos(pos)
|
||||
bomb:SetAngles(velocity:Angle())
|
||||
bomb:Spawn()
|
||||
bomb:Activate()
|
||||
|
||||
local phys = bomb:GetPhysicsObject()
|
||||
if (IsValid(phys)) then
|
||||
phys:SetVelocity(velocity)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:DropCrate(pos, weapon)
|
||||
if (!weapon) then
|
||||
local weaponsConfig = ix.config.Get("airdropWeapons", "")
|
||||
local weapons = {}
|
||||
|
||||
if (type(weaponsConfig) == "string" and weaponsConfig:Trim() != "") then
|
||||
for _, v in pairs(string.Split(weaponsConfig, "\n")) do
|
||||
local s = v:Trim()
|
||||
if (s != "") then
|
||||
table.insert(weapons, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Если конфиг пустой или старый, берем список по умолчанию
|
||||
if (#weapons == 0) then
|
||||
weapons = {
|
||||
"tacrp_io_degala",
|
||||
"tacrp_io_fiveseven",
|
||||
"tacrp_mr96",
|
||||
"tacrp_pdw",
|
||||
"tacrp_superv",
|
||||
"tacrp_sd_aac_hb",
|
||||
"tacrp_ak_ak12",
|
||||
"tacrp_ak_an94",
|
||||
"tacrp_sg551",
|
||||
"tacrp_io_xm8car",
|
||||
"tacrp_hk417",
|
||||
"tacrp_io_scarh",
|
||||
"tacrp_mg4",
|
||||
"tacrp_io_xm8lmg",
|
||||
"tacrp_io_sg550r",
|
||||
"tacrp_io_sl8",
|
||||
"tacrp_io_sg550",
|
||||
"tacrp_io_vss",
|
||||
"tacrp_as50",
|
||||
"tacrp_ex_hecate",
|
||||
"tacrp_civ_m320"
|
||||
}
|
||||
end
|
||||
|
||||
weapon = weapons[math.random(#weapons)]
|
||||
end
|
||||
|
||||
local crate = ents.Create("ix_airdrop")
|
||||
if (!IsValid(crate)) then
|
||||
print("[Airdrop] ОШИБКА: Не удалось создать энтити ix_airdrop!")
|
||||
return
|
||||
end
|
||||
crate:SetPos(pos)
|
||||
crate:SetAngles(Angle(0, math.random(0, 360), 0))
|
||||
crate:Spawn()
|
||||
crate.ixWeapon = weapon
|
||||
|
||||
-- Создаем парашют
|
||||
util.PrecacheModel("models/jessev92/bf2/parachute.mdl")
|
||||
local parachute = ents.Create("prop_dynamic")
|
||||
parachute:SetModel("models/jessev92/bf2/parachute.mdl")
|
||||
parachute:SetPos(crate:GetPos() + Vector(0, 0, 50))
|
||||
parachute:SetParent(crate)
|
||||
parachute:Spawn()
|
||||
crate.ixParachute = parachute
|
||||
|
||||
local phys = crate:GetPhysicsObject()
|
||||
if (IsValid(phys)) then
|
||||
phys:Wake()
|
||||
phys:SetVelocity(Vector(0, 0, -200)) -- Скорость падения
|
||||
phys:SetDragCoefficient(5)
|
||||
phys:SetAngleDragCoefficient(100)
|
||||
end
|
||||
|
||||
local function StartSmoke()
|
||||
if (!IsValid(crate)) then return end
|
||||
local smoke = ents.Create("env_smokestack")
|
||||
smoke:SetPos(crate:GetPos())
|
||||
smoke:SetAngles(Angle(-90, 0, 0))
|
||||
smoke:SetKeyValue("InitialState", "1")
|
||||
smoke:SetKeyValue("rendercolor", "255 100 0")
|
||||
smoke:SetKeyValue("renderamt", "255")
|
||||
smoke:SetKeyValue("SmokeMaterial", "particle/smokesprites_0001.vmt")
|
||||
smoke:SetKeyValue("BaseSpread", "20")
|
||||
smoke:SetKeyValue("SpreadSpeed", "10")
|
||||
smoke:SetKeyValue("Speed", "80")
|
||||
smoke:SetKeyValue("StartSize", "30")
|
||||
smoke:SetKeyValue("EndSize", "120")
|
||||
smoke:SetKeyValue("Rate", "40")
|
||||
smoke:SetKeyValue("JetLength", "300")
|
||||
smoke:Spawn()
|
||||
smoke:Activate()
|
||||
smoke:SetParent(crate)
|
||||
crate.ixSmoke = smoke
|
||||
ix.util.Notify("Аирдроп приземлился! Ищите оранжевый дым.", nil, "all")
|
||||
end
|
||||
|
||||
-- Принудительно держим ящик ровно во время падения
|
||||
timer.Create("AirdropUpright_" .. crate:EntIndex(), 0.1, 0, function()
|
||||
if (!IsValid(crate)) then
|
||||
timer.Remove("AirdropUpright_" .. crate:EntIndex())
|
||||
return
|
||||
end
|
||||
|
||||
local phys = crate:GetPhysicsObject()
|
||||
if (IsValid(phys) and phys:IsMotionEnabled()) then
|
||||
crate:SetAngles(Angle(0, crate:GetAngles().y, 0))
|
||||
phys:SetAngleVelocity(Vector(0, 0, 0))
|
||||
else
|
||||
timer.Remove("AirdropUpright_" .. crate:EntIndex())
|
||||
end
|
||||
end)
|
||||
|
||||
timer.Create("AirdropLand_" .. crate:EntIndex(), 0.2, 0, function()
|
||||
if (!IsValid(crate)) then
|
||||
timer.Remove("AirdropLand_" .. crate:EntIndex())
|
||||
return
|
||||
end
|
||||
local phys = crate:GetPhysicsObject()
|
||||
if (!IsValid(phys)) then return end
|
||||
|
||||
-- Если скорость мала или мы коснулись земли
|
||||
local tr = util.TraceLine({
|
||||
start = crate:GetPos() + Vector(0, 0, 10),
|
||||
endpos = crate:GetPos() - Vector(0, 0, 40),
|
||||
filter = crate
|
||||
})
|
||||
|
||||
if (tr.Hit or phys:GetVelocity():Length() < 5) then
|
||||
phys:EnableMotion(false)
|
||||
crate:SetPos(tr.HitPos)
|
||||
crate:SetAngles(Angle(0, crate:GetAngles().y, 0))
|
||||
|
||||
if (IsValid(crate.ixParachute)) then
|
||||
crate.ixParachute:Remove()
|
||||
end
|
||||
|
||||
StartSmoke()
|
||||
timer.Remove("AirdropLand_" .. crate:EntIndex())
|
||||
timer.Remove("AirdropUpright_" .. crate:EntIndex())
|
||||
end
|
||||
end)
|
||||
|
||||
timer.Simple(900, function()
|
||||
if (IsValid(crate)) then
|
||||
if (IsValid(crate.ixSmoke)) then crate.ixSmoke:Remove() end
|
||||
crate:Remove()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:Initialize()
|
||||
self:SetupAirdropTimer()
|
||||
self:SetupAirstrikeTimer()
|
||||
end
|
||||
|
||||
function PLUGIN:SetupAirdropTimer()
|
||||
local delay = math.random(ix.config.Get("airdropMinInterval", 3600), ix.config.Get("airdropMaxInterval", 7200))
|
||||
timer.Create("ixAirdropTimer", delay, 1, function()
|
||||
self:SpawnAirdrop()
|
||||
self:SetupAirdropTimer()
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:SetupAirstrikeTimer()
|
||||
local delay = math.random(ix.config.Get("airstrikeMinInterval", 1800), ix.config.Get("airstrikeMaxInterval", 3600))
|
||||
timer.Create("ixAirstrikeTimer", delay, 1, function()
|
||||
self:SpawnAirstrike()
|
||||
self:SetupAirstrikeTimer()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,553 @@
|
||||
-- Цветовая схема (в стиле остальных интерфейсов)
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
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_HOVER = Color(33, 110, 38)
|
||||
local COLOR_ACCENT = Color(56, 102, 35)
|
||||
local COLOR_ACCENT_HOVER = Color(66, 120, 45)
|
||||
local COLOR_BORDER = Color(46, 125, 50, 80)
|
||||
local COLOR_TEXT_PRIMARY = Color(165, 214, 167)
|
||||
local COLOR_TEXT_SECONDARY = Color(129, 199, 132)
|
||||
local COLOR_TEXT_DIM = Color(100, 150, 105)
|
||||
|
||||
-- Хук для камеры во время анимации
|
||||
function PLUGIN:CalcView(ply, pos, angles, fov)
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" then
|
||||
if GetConVar("rp_taunt_firstperson"):GetBool() then
|
||||
local eye_id = ply:LookupAttachment('eyes')
|
||||
local att = eye_id > 0 and ply:GetAttachment(eye_id) or nil
|
||||
|
||||
if not att then return end
|
||||
|
||||
local ang1 = angles
|
||||
if GetConVar("rp_taunt_realistic_firstperson"):GetBool() then
|
||||
ang1 = att.Ang
|
||||
end
|
||||
local view = {
|
||||
origin = att.Pos,
|
||||
angles = ang1,
|
||||
fov = fov,
|
||||
drawviewer = true
|
||||
}
|
||||
return view
|
||||
else
|
||||
local view = {
|
||||
origin = pos - (angles:Forward() * 100),
|
||||
angles = angles,
|
||||
fov = fov,
|
||||
drawviewer = true
|
||||
}
|
||||
return view
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ShouldDrawLocalPlayer(ply)
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" and not GetConVar("rp_taunt_firstperson"):GetBool() then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Подсказка на экране
|
||||
function PLUGIN:HUDPaint()
|
||||
local ply = LocalPlayer()
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" then
|
||||
draw.SimpleTextOutlined("Нажмите SHIFT, чтобы убрать анимацию.", "Trebuchet18", ScrW()/2, ScrH()-250, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER, 1, color_black)
|
||||
end
|
||||
end
|
||||
|
||||
-- Таблица анимаций (импортируем из существующего аддона)
|
||||
local AnimationCategories = {
|
||||
{
|
||||
name = "ЖЕСТЫ",
|
||||
anims = {
|
||||
{id = 1, name = "Помахать руками"},
|
||||
{id = 2, name = "Помахать рукой"},
|
||||
{id = 3, name = "Показать пальцем вперед"},
|
||||
{id = 4, name = "Показать пальцем назад"},
|
||||
{id = 5, name = "Поправить галстук"},
|
||||
}
|
||||
},
|
||||
{
|
||||
name = "СПОРТ",
|
||||
anims = {
|
||||
{id = 6, name = "Приседания"},
|
||||
{id = 7, name = "Отжимания"},
|
||||
{id = 8, name = "Подъем корпуса"},
|
||||
{id = 9, name = "Берпи"},
|
||||
}
|
||||
},
|
||||
{
|
||||
name = "ПОЗЫ",
|
||||
anims = {
|
||||
{id = 10, name = "Стоять злобно"},
|
||||
{id = 11, name = "Стоять напуганно"},
|
||||
{id = 12, name = "Стоять, сложив руки"},
|
||||
{id = 13, name = "Стоять, руки на поясе"},
|
||||
{id = 14, name = "Стоять, держась за пояс"},
|
||||
{id = 15, name = "Сидеть, рука на колене"},
|
||||
{id = 16, name = "Сидеть в позе лотоса"},
|
||||
{id = 17, name = "Сидеть в сонном состоянии"},
|
||||
{id = 18, name = "Лежать в плохом состоянии"},
|
||||
}
|
||||
},
|
||||
{
|
||||
name = "ТАНЦЫ",
|
||||
anims = {
|
||||
{id = 19, name = "Танец 1"},
|
||||
{id = 20, name = "Танец 2"},
|
||||
{id = 21, name = "Танец 3"},
|
||||
{id = 22, name = "Танец 4"},
|
||||
{id = 33, name = "Танец в присядь"},
|
||||
}
|
||||
},
|
||||
{
|
||||
name = "ДЕЙСТВИЯ",
|
||||
anims = {
|
||||
{id = 23, name = "Согласие"},
|
||||
{id = 24, name = "Несогласие"},
|
||||
{id = 25, name = "Позвать с собой"},
|
||||
{id = 26, name = "Поклониться"},
|
||||
{id = 27, name = "Отдать честь"},
|
||||
{id = 28, name = "Пить кофе"},
|
||||
{id = 29, name = "Смотреть на объект"},
|
||||
{id = 30, name = "Записывать в блокнот"},
|
||||
{id = 31, name = "Спать"},
|
||||
{id = 32, name = "Воинское приветствие"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local animMenu = nil
|
||||
local animMenuOpen = false
|
||||
|
||||
-- Функция создания меню анимаций
|
||||
local function CreateAnimationMenu()
|
||||
if IsValid(animMenu) then
|
||||
animMenu:Remove()
|
||||
end
|
||||
|
||||
-- Невидимая панель на весь экран для перехвата ввода
|
||||
local basePanel = vgui.Create("DPanel")
|
||||
basePanel:SetSize(ScrW(), ScrH())
|
||||
basePanel:SetPos(0, 0)
|
||||
basePanel:MakePopup()
|
||||
basePanel.Paint = function() end -- Полностью прозрачная
|
||||
|
||||
-- Меню в правом верхнем углу
|
||||
local menuWidth = 320
|
||||
local menuHeight = 500
|
||||
local padding = 20
|
||||
|
||||
local menu = vgui.Create("DPanel", basePanel)
|
||||
menu:SetSize(menuWidth, menuHeight)
|
||||
menu:SetPos(ScrW() - menuWidth - padding, padding)
|
||||
menu.Paint = function(s, w, h)
|
||||
-- Основной фон
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK)
|
||||
|
||||
-- Заголовок
|
||||
draw.RoundedBoxEx(8, 0, 0, w, 45, COLOR_BG_MEDIUM, true, true, false, false)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawLine(0, 45, w, 45)
|
||||
|
||||
-- Текст заголовка
|
||||
draw.SimpleText("АНИМАЦИИ", "DermaLarge", w/2, 22, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Декоративная линия
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(10, 43, w-20, 2)
|
||||
|
||||
-- Внешняя граница
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
|
||||
-- Подсказка внизу
|
||||
draw.SimpleText("C - Закрыть | SHIFT - Остановить анимацию", "DermaDefault", w/2, h-15, COLOR_TEXT_DIM, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- Кнопка закрытия
|
||||
local closeBtn = vgui.Create("DButton", menu)
|
||||
closeBtn:SetPos(menuWidth - 35, 8)
|
||||
closeBtn:SetSize(28, 28)
|
||||
closeBtn:SetText("")
|
||||
closeBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and COLOR_ACCENT_HOVER or COLOR_ACCENT
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
draw.SimpleText("×", "DermaLarge", w/2, h/2-2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
closeBtn.DoClick = function()
|
||||
basePanel:Remove()
|
||||
animMenuOpen = false
|
||||
end
|
||||
|
||||
-- Скролл панель для категорий
|
||||
local scroll = vgui.Create("DScrollPanel", menu)
|
||||
scroll:SetPos(10, 55)
|
||||
scroll:SetSize(menuWidth - 20, menuHeight - 90)
|
||||
|
||||
local sbar = scroll:GetVBar()
|
||||
sbar:SetWide(6)
|
||||
sbar.Paint = function(s, w, h)
|
||||
draw.RoundedBox(3, 0, 0, w, h, COLOR_BG_MEDIUM)
|
||||
end
|
||||
sbar.btnGrip.Paint = function(s, w, h)
|
||||
draw.RoundedBox(3, 0, 0, w, h, COLOR_ACCENT)
|
||||
end
|
||||
sbar.btnUp.Paint = function() end
|
||||
sbar.btnDown.Paint = function() end
|
||||
|
||||
-- Создаем категории с анимациями
|
||||
for _, category in ipairs(AnimationCategories) do
|
||||
-- Заголовок категории
|
||||
local catHeader = vgui.Create("DPanel", scroll)
|
||||
catHeader:Dock(TOP)
|
||||
catHeader:DockMargin(0, 5, 0, 3)
|
||||
catHeader:SetTall(28)
|
||||
catHeader.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
draw.SimpleText(category.name, "DermaDefaultBold", 10, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- Анимации в категории
|
||||
for _, anim in ipairs(category.anims) do
|
||||
local animBtn = vgui.Create("DButton", scroll)
|
||||
animBtn:Dock(TOP)
|
||||
animBtn:DockMargin(3, 2, 3, 0)
|
||||
animBtn:SetTall(32)
|
||||
animBtn:SetText("")
|
||||
|
||||
animBtn.Paint = function(s, w, h)
|
||||
local col = COLOR_BG_MEDIUM
|
||||
if s:IsHovered() then
|
||||
col = COLOR_PRIMARY_HOVER
|
||||
-- Акцентная линия слева
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(0, 0, 3, h)
|
||||
end
|
||||
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
|
||||
-- Иконка
|
||||
draw.SimpleText("▶", "DermaDefault", 12, h/2, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Название анимации
|
||||
draw.SimpleText(anim.name, "DermaDefault", 28, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
animBtn.DoClick = function()
|
||||
RunConsoleCommand("rp_set_taunt", tostring(anim.id))
|
||||
surface.PlaySound("buttons/button15.wav")
|
||||
basePanel:Remove()
|
||||
animMenuOpen = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Обработка закрытия по нажатию вне меню
|
||||
basePanel.OnMousePressed = function(s, keyCode)
|
||||
if keyCode == MOUSE_LEFT then
|
||||
-- Проверяем, кликнули ли вне меню
|
||||
local mx, my = input.GetCursorPos()
|
||||
local x, y = menu:GetPos()
|
||||
local w, h = menu:GetSize()
|
||||
|
||||
if mx < x or mx > x + w or my < y or my > y + h then
|
||||
basePanel:Remove()
|
||||
animMenuOpen = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Меню рации в левом верхнем углу
|
||||
local radioWidth = 320
|
||||
local radioHeight = 200
|
||||
|
||||
local radioMenu = vgui.Create("DPanel", basePanel)
|
||||
radioMenu:SetSize(radioWidth, radioHeight)
|
||||
radioMenu:SetPos(padding, padding)
|
||||
radioMenu.Paint = function(s, w, h)
|
||||
-- Основной фон
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK)
|
||||
|
||||
-- Заголовок
|
||||
draw.RoundedBoxEx(8, 0, 0, w, 45, COLOR_BG_MEDIUM, true, true, false, false)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawLine(0, 45, w, 45)
|
||||
|
||||
-- Текст заголовка
|
||||
draw.SimpleText("ЧАСТОТА РАЦИИ", "DermaLarge", w/2, 22, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Декоративная линия
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(10, 43, w-20, 2)
|
||||
|
||||
-- Внешняя граница
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
-- Получаем плагин рации
|
||||
local radioPlugin = ix.plugin.list["radio"]
|
||||
if not radioPlugin then
|
||||
radioMenu:SetVisible(false)
|
||||
else
|
||||
local currentFreq = radioPlugin:GetFrequency(LocalPlayer())
|
||||
|
||||
-- Текущая частота
|
||||
local freqLabel = vgui.Create("DLabel", radioMenu)
|
||||
freqLabel:SetPos(20, 60)
|
||||
freqLabel:SetSize(radioWidth - 40, 25)
|
||||
freqLabel:SetText("Текущая частота: " .. string.format("%0." .. radioPlugin.frequencyPrecision .. "f", currentFreq))
|
||||
freqLabel:SetFont("DermaDefault")
|
||||
freqLabel:SetTextColor(COLOR_TEXT_PRIMARY)
|
||||
|
||||
-- Слайдер частоты
|
||||
local freqSlider = vgui.Create("DNumSlider", radioMenu)
|
||||
freqSlider:SetPos(10, 90)
|
||||
freqSlider:SetSize(radioWidth - 20, 40)
|
||||
freqSlider:SetText("")
|
||||
freqSlider:SetMin(radioPlugin.minFrequency)
|
||||
freqSlider:SetMax(radioPlugin.maxFrequency)
|
||||
freqSlider:SetDecimals(radioPlugin.frequencyPrecision)
|
||||
freqSlider:SetValue(currentFreq)
|
||||
|
||||
freqSlider.Label:SetTextColor(COLOR_TEXT_SECONDARY)
|
||||
freqSlider.Label:SetFont("DermaDefault")
|
||||
freqSlider.TextArea:SetTextColor(COLOR_TEXT_PRIMARY)
|
||||
freqSlider.TextArea:SetFont("DermaDefault")
|
||||
freqSlider.TextArea:SetEditable(true)
|
||||
freqSlider.TextArea:SetNumeric(true)
|
||||
freqSlider.TextArea.Paint = function(panel, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_MEDIUM)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
panel:DrawTextEntryText(COLOR_TEXT_PRIMARY, COLOR_ACCENT, COLOR_TEXT_PRIMARY)
|
||||
end
|
||||
|
||||
-- Обработчик ввода вручную
|
||||
freqSlider.TextArea.OnEnter = function(panel)
|
||||
local val = tonumber(panel:GetValue())
|
||||
if val then
|
||||
val = math.Clamp(val, radioPlugin.minFrequency, radioPlugin.maxFrequency)
|
||||
freqSlider:SetValue(val)
|
||||
end
|
||||
end
|
||||
|
||||
freqSlider.Slider.Paint = function(panel, w, h)
|
||||
draw.RoundedBox(4, 0, h/2-2, w, 4, COLOR_BG_MEDIUM)
|
||||
local progress = (freqSlider:GetValue() - freqSlider:GetMin()) / (freqSlider:GetMax() - freqSlider:GetMin())
|
||||
draw.RoundedBox(4, 0, h/2-2, w * progress, 4, COLOR_ACCENT)
|
||||
end
|
||||
|
||||
freqSlider.Slider.Knob.Paint = function(s, w, h)
|
||||
draw.RoundedBox(w/2, 0, 0, w, h, COLOR_PRIMARY)
|
||||
if s:IsHovered() then
|
||||
draw.RoundedBox(w/2, 2, 2, w-4, h-4, COLOR_ACCENT_HOVER)
|
||||
end
|
||||
end
|
||||
|
||||
freqSlider.OnValueChanged = function(_, value)
|
||||
local format = "%0." .. radioPlugin.frequencyPrecision .. "f"
|
||||
freqLabel:SetText("Текущая частота: " .. string.format(format, value))
|
||||
end
|
||||
|
||||
-- Кнопка применения
|
||||
local btnWidth = (radioWidth - 50) / 2
|
||||
local applyBtn = vgui.Create("DButton", radioMenu)
|
||||
applyBtn:SetPos(20, 150)
|
||||
applyBtn:SetSize(btnWidth, 35)
|
||||
applyBtn:SetText("")
|
||||
applyBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and COLOR_PRIMARY_HOVER or COLOR_PRIMARY
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
|
||||
draw.SimpleText("ПРИМЕНИТЬ", "DermaDefaultBold", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
applyBtn.DoClick = function()
|
||||
net.Start("ixRadioSetFrequency")
|
||||
net.WriteFloat(freqSlider:GetValue())
|
||||
net.SendToServer()
|
||||
|
||||
LocalPlayer():Notify("Частота рации изменена на: " .. string.format("%0." .. radioPlugin.frequencyPrecision .. "f", freqSlider:GetValue()))
|
||||
end
|
||||
|
||||
-- Кнопка ручного ввода
|
||||
local manualBtn = vgui.Create("DButton", radioMenu)
|
||||
manualBtn:SetPos(30 + btnWidth, 150)
|
||||
manualBtn:SetSize(btnWidth, 35)
|
||||
manualBtn:SetText("")
|
||||
manualBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and COLOR_ACCENT_HOVER or COLOR_ACCENT
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
|
||||
draw.SimpleText("ВВЕСТИ", "DermaDefaultBold", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
manualBtn.DoClick = function()
|
||||
Derma_StringRequest(
|
||||
"Ввод частоты",
|
||||
"Введите частоту рации (от " .. radioPlugin.minFrequency .. " до " .. radioPlugin.maxFrequency .. "):",
|
||||
tostring(currentFreq),
|
||||
function(text)
|
||||
local freq = tonumber(text)
|
||||
if freq then
|
||||
freq = math.Clamp(freq, radioPlugin.minFrequency, radioPlugin.maxFrequency)
|
||||
|
||||
net.Start("ixRadioSetFrequency")
|
||||
net.WriteFloat(freq)
|
||||
net.SendToServer()
|
||||
|
||||
freqSlider:SetValue(freq)
|
||||
LocalPlayer():Notify("Частота рации изменена на: " .. string.format("%0." .. radioPlugin.frequencyPrecision .. "f", freq))
|
||||
else
|
||||
LocalPlayer():Notify("Неверный формат частоты!")
|
||||
end
|
||||
end,
|
||||
function() end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Кнопки включения/выключения рации (справа от меню рации)
|
||||
if radioPlugin then
|
||||
local controlsWidth = 150
|
||||
local controlsHeight = 200
|
||||
|
||||
local controlsPanel = vgui.Create("DPanel", basePanel)
|
||||
controlsPanel:SetSize(controlsWidth, controlsHeight)
|
||||
controlsPanel:SetPos(padding + radioWidth + 10, padding)
|
||||
controlsPanel.Paint = function(s, w, h)
|
||||
-- Основной фон
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK)
|
||||
|
||||
-- Заголовок
|
||||
draw.RoundedBoxEx(8, 0, 0, w, 45, COLOR_BG_MEDIUM, true, true, false, false)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawLine(0, 45, w, 45)
|
||||
|
||||
-- Текст заголовка
|
||||
draw.SimpleText("РАЦИЯ", "DermaDefault", w/2, 22, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Декоративная линия
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(10, 43, w-20, 2)
|
||||
|
||||
-- Внешняя граница
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
-- Кнопка включения прослушивания
|
||||
local listenBtn = vgui.Create("DButton", controlsPanel)
|
||||
listenBtn:SetPos(10, 60)
|
||||
listenBtn:SetSize(controlsWidth - 20, 40)
|
||||
listenBtn:SetText("")
|
||||
listenBtn.Paint = function(s, w, h)
|
||||
local isListening = radioPlugin:IsListening(LocalPlayer())
|
||||
local col = isListening and COLOR_ACCENT or COLOR_BG_MEDIUM
|
||||
if s:IsHovered() then
|
||||
col = isListening and COLOR_ACCENT_HOVER or COLOR_PRIMARY_HOVER
|
||||
end
|
||||
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
|
||||
local status = isListening and "ВКЛ" or "ВЫКЛ"
|
||||
draw.SimpleText("Прослушивание", "DermaDefault", w/2, h/2 - 7, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText(status, "DermaDefaultBold", w/2, h/2 + 7, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
listenBtn.DoClick = function()
|
||||
net.Start("ixRadioToggleListen")
|
||||
net.SendToServer()
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
local status = radioPlugin:IsListening(LocalPlayer()) and "включено" or "выключено"
|
||||
LocalPlayer():Notify("Прослушивание рации " .. status)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Кнопка включения передачи
|
||||
local transmitBtn = vgui.Create("DButton", controlsPanel)
|
||||
transmitBtn:SetPos(10, 110)
|
||||
transmitBtn:SetSize(controlsWidth - 20, 40)
|
||||
transmitBtn:SetText("")
|
||||
transmitBtn.Paint = function(s, w, h)
|
||||
local isTransmitting = radioPlugin:IsTransmitting(LocalPlayer())
|
||||
local col = isTransmitting and COLOR_ACCENT or COLOR_BG_MEDIUM
|
||||
if s:IsHovered() then
|
||||
col = isTransmitting and COLOR_ACCENT_HOVER or COLOR_PRIMARY_HOVER
|
||||
end
|
||||
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
|
||||
local status = isTransmitting and "ВКЛ" or "ВЫКЛ"
|
||||
draw.SimpleText("Передача", "DermaDefault", w/2, h/2 - 7, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText(status, "DermaDefaultBold", w/2, h/2 + 7, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
transmitBtn.DoClick = function()
|
||||
net.Start("ixRadioToggleTransmit")
|
||||
net.SendToServer()
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
local status = radioPlugin:IsTransmitting(LocalPlayer()) and "включена" or "выключена"
|
||||
LocalPlayer():Notify("Передача рации " .. status)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
animMenu = basePanel
|
||||
animMenuOpen = true
|
||||
end
|
||||
|
||||
-- Блокировка стандартного Context Menu и открытие нашего
|
||||
hook.Add("OnContextMenuOpen", "AnimationMenuOverride", function()
|
||||
-- Проверка на Alt+C для администраторов (открывает стандартное меню)
|
||||
if input.IsKeyDown(KEY_LALT) or input.IsKeyDown(KEY_RALT) then
|
||||
local client = LocalPlayer()
|
||||
if IsValid(client) and client:GetCharacter() then
|
||||
-- Проверка на права администратора
|
||||
if client:IsAdmin() or client:IsSuperAdmin() then
|
||||
return -- Пропускаем и открываем стандартное меню
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not animMenuOpen then
|
||||
CreateAnimationMenu()
|
||||
end
|
||||
return false -- Блокируем стандартное меню
|
||||
end)
|
||||
|
||||
-- Закрытие меню при закрытии Context Menu
|
||||
hook.Add("OnContextMenuClose", "AnimationMenuClose", function()
|
||||
if animMenuOpen and IsValid(animMenu) then
|
||||
animMenu:Remove()
|
||||
animMenuOpen = false
|
||||
end
|
||||
end)
|
||||
|
||||
-- Подсказка на HUD
|
||||
--hook.Add("HUDPaint", "AnimationMenuHint", function()
|
||||
-- if not animMenuOpen then
|
||||
-- draw.SimpleTextOutlined("C - Открыть меню (анимации + рация)", "DermaDefault", ScrW() - 10, ScrH() - 30, COLOR_TEXT_DIM, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER, 1, Color(0, 0, 0, 200))
|
||||
-- end
|
||||
--end)
|
||||
@@ -0,0 +1,192 @@
|
||||
local PLUGIN = PLUGIN
|
||||
PLUGIN.name = "Animation Context Menu"
|
||||
PLUGIN.author = "RefoselTeamWork"
|
||||
PLUGIN.description = "Замена стандартного Context Menu на меню выбора анимаций"
|
||||
|
||||
PLUGIN.AnimationTable = {
|
||||
[1] = {name = "Помахать руками", anim = "rp_wave", loop = false},
|
||||
[2] = {name = "Помахать рукой", anim = "gesture_wave_original", loop = false},
|
||||
[3] = {name = "Показать пальцем вперед", anim = "rp_point", loop = false},
|
||||
[4] = {name = "Показать пальцем назад", anim = "rp_point_back", loop = false},
|
||||
[5] = {name = "Поправить галстук", anim = "menu_gman", loop = false},
|
||||
[6] = {name = "Приседания", anim = "rp_sport1_loop", loop = true},
|
||||
[7] = {name = "Отжимания", anim = "rp_sport2_loop", loop = true},
|
||||
[8] = {name = "Подъем корпуса", anim = "rp_sport3_loop", loop = true},
|
||||
[9] = {name = "Берпи", anim = "rp_sport4_loop", loop = true},
|
||||
[10] = {name = "Стоять злобно", anim = "rp_angry_loop", loop = true},
|
||||
[11] = {name = "Стоять напуганно", anim = "idle_all_scared", loop = true},
|
||||
[12] = {name = "Стоять, сложив руки", anim = "pose_standing_01", loop = true},
|
||||
[13] = {name = "Стоять, руки на поясе", anim = "pose_standing_02", loop = true},
|
||||
[14] = {name = "Стоять, держась за пояс", anim = "rp_cop_idle", loop = true},
|
||||
[15] = {name = "Сидеть, рука на колене", anim = "pose_ducking_01", loop = true},
|
||||
[16] = {name = "Сидеть в позе лотоса", anim = "pose_ducking_02", loop = true},
|
||||
[17] = {name = "Сидеть в сонном состоянии", anim = "rp_sit_loop", loop = true},
|
||||
[18] = {name = "Лежать в плохом состоянии", anim = "rp_injured_loop", loop = true},
|
||||
[19] = {name = "Танец 1", anim = "rp_dance1_loop", loop = true},
|
||||
[20] = {name = "Танец 2", anim = "rp_dance2_loop", loop = true},
|
||||
[21] = {name = "Танец 3", anim = "rp_dance3_loop", loop = true},
|
||||
[23] = {name = "Согласие", anim = "gesture_agree_original", loop = false},
|
||||
[24] = {name = "Несогласие", anim = "gesture_disagree_original", loop = false},
|
||||
[25] = {name = "Позвать с собой", anim = "gesture_becon_original", loop = false},
|
||||
[26] = {name = "Поклониться", anim = "gesture_bow_original", loop = false},
|
||||
[27] = {name = "Отдать честь", anim = "gesture_salute_original", loop = false},
|
||||
[28] = {name = "Пить кофе", anim = "rp_drinking", loop = true, prop = {model = "models/props_junk/garbage_coffeemug001a.mdl", spawnfunc = function(mod, ent)
|
||||
local att = ent:LookupAttachment("anim_attachment_RH")
|
||||
local lvec = Vector(1,0,1)
|
||||
|
||||
timer.Create("WhileThatPropExist"..ent:EntIndex(), 0, 0, function()
|
||||
if IsValid(mod) then
|
||||
local tab = ent:GetAttachment(att)
|
||||
|
||||
if (tab) then
|
||||
mod:SetPos(tab.Pos+tab.Ang:Up()*lvec.z+tab.Ang:Right()*lvec.y+tab.Ang:Forward()*lvec.x)
|
||||
mod:SetAngles(tab.Ang)
|
||||
end
|
||||
|
||||
if !IsValid(ent) or !ent:Alive() then
|
||||
timer.Remove("WhileThatPropExist"..ent:EntIndex())
|
||||
mod:Remove()
|
||||
end
|
||||
else
|
||||
timer.Remove("WhileThatPropExist"..ent:EntIndex())
|
||||
end
|
||||
end)
|
||||
end}},
|
||||
[29] = {name = "Смотреть на объект", anim = "rp_medic_idle", loop = true},
|
||||
[30] = {name = "Записывать в блокнот", anim = "rp_writing", loop = true, prop = {model = "models/props_lab/clipboard.mdl", spawnfunc = function(mod, ent)
|
||||
local att = ent:LookupAttachment("anim_attachment_RH")
|
||||
local lvec = Vector(2,-2,2)
|
||||
|
||||
timer.Create("WhileThatPropExist"..ent:EntIndex(), 0, 0, function()
|
||||
if IsValid(mod) then
|
||||
local tab = ent:GetAttachment(att)
|
||||
|
||||
if (tab) then
|
||||
mod:SetPos(tab.Pos+tab.Ang:Up()*lvec.z+tab.Ang:Right()*lvec.y+tab.Ang:Forward()*lvec.x)
|
||||
mod:SetAngles(tab.Ang+Angle(140,0,-100))
|
||||
end
|
||||
|
||||
if !IsValid(ent) or !ent:Alive() then
|
||||
timer.Remove("WhileThatPropExist"..ent:EntIndex())
|
||||
mod:Remove()
|
||||
end
|
||||
else
|
||||
timer.Remove("WhileThatPropExist"..ent:EntIndex())
|
||||
end
|
||||
end)
|
||||
end}},
|
||||
[31] = {name = "Спать", anim = "rp_sleep", loop = true},
|
||||
[32] = {name = "Воинское приветствие", anim = "rp_salute1", loop = true, loop_stop = true},
|
||||
[33] = {name = "Танец в присядь", anim = "rp_dance_squat", loop = true},
|
||||
}
|
||||
|
||||
if SERVER then
|
||||
concommand.Add("rp_set_taunt", function(ply, cmd, args)
|
||||
local arg = tonumber(args[1])
|
||||
if arg and arg > 0 and arg <= #PLUGIN.AnimationTable then
|
||||
local tab = PLUGIN.AnimationTable[arg]
|
||||
PLUGIN:SetTAnimation(ply, tab.anim, not tab.loop, arg)
|
||||
end
|
||||
end)
|
||||
|
||||
function PLUGIN:SetTAnimation(ply, anim, autostop, id)
|
||||
ply:SetNW2String('TauntAnim', anim)
|
||||
ply:SetNW2Float('TauntID', id)
|
||||
ply:SetNW2Float('TAnimDelay', select(2, ply:LookupSequence(anim)))
|
||||
ply:SetNW2Float('TAnimStartTime', CurTime())
|
||||
ply:SetCycle(0)
|
||||
|
||||
local wep = ply:GetActiveWeapon()
|
||||
if IsValid(wep) and anim != "" then
|
||||
ply.TauntPreviousWeapon = wep:GetClass()
|
||||
ply:SetActiveWeapon(nil)
|
||||
elseif anim == "" and isstring(ply.TauntPreviousWeapon) then
|
||||
ply:SelectWeapon(ply.TauntPreviousWeapon)
|
||||
ply.TauntPreviousWeapon = nil
|
||||
end
|
||||
|
||||
if autostop then
|
||||
local delay = select(2, ply:LookupSequence(anim))
|
||||
timer.Create("TauntAGRP"..ply:EntIndex(), delay, 1, function()
|
||||
if not IsValid(ply) then return end
|
||||
local anim2 = ply:GetNW2String('TauntAnim')
|
||||
if anim == anim2 then
|
||||
PLUGIN:SetTAnimation(ply, "")
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerSwitchWeapon(ply, oldWeapon, newWeapon)
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:Think()
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" and (ply:InVehicle() or not ply:Alive() or ply:WaterLevel() >= 2) then
|
||||
PLUGIN:SetTAnimation(ply, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
CreateConVar("rp_taunt_firstperson", 0, FCVAR_ARCHIVE, "", 0, 1)
|
||||
CreateConVar("rp_taunt_realistic_firstperson", 0, FCVAR_ARCHIVE, "", 0, 1)
|
||||
end
|
||||
|
||||
function PLUGIN:SetupMove(ply, mvd, cmd)
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
if str != "" then
|
||||
mvd:SetMaxSpeed(1)
|
||||
mvd:SetMaxClientSpeed(1)
|
||||
if SERVER and ply:KeyDown(IN_SPEED) then
|
||||
PLUGIN:SetTAnimation(ply, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:CalcMainActivity(ply, vel)
|
||||
local str = ply:GetNW2String('TauntAnim')
|
||||
local num = ply:GetNW2Float('TAnimDelay')
|
||||
local id = ply:GetNW2Float('TauntID')
|
||||
local st = ply:GetNW2Float('TAnimStartTime')
|
||||
|
||||
if str != "" and ply:Alive() then
|
||||
local ls = PLUGIN.AnimationTable[id] and PLUGIN.AnimationTable[id].loop_stop
|
||||
|
||||
if ply:GetCycle() >= 1 then
|
||||
if not ls then
|
||||
ply:SetCycle(0)
|
||||
if SERVER then
|
||||
ply:SetNW2Float('TAnimStartTime', CurTime())
|
||||
end
|
||||
end
|
||||
else
|
||||
ply:SetCycle((CurTime()-st)/num)
|
||||
|
||||
local tab = PLUGIN.AnimationTable[id] and PLUGIN.AnimationTable[id].prop
|
||||
if CLIENT and istable(tab) and (not IsValid(ply.TauntProp) or ply.TauntProp:GetModel() != tab.model) then
|
||||
if IsValid(ply.TauntProp) then
|
||||
ply.TauntProp:Remove()
|
||||
end
|
||||
ply.TauntProp = ClientsideModel(tab.model)
|
||||
tab.spawnfunc(ply.TauntProp, ply)
|
||||
elseif CLIENT and not istable(tab) and IsValid(ply.TauntProp) then
|
||||
ply.TauntProp:Remove()
|
||||
end
|
||||
end
|
||||
|
||||
return -1, ply:LookupSequence(str)
|
||||
else
|
||||
if SERVER then
|
||||
ply:SetNW2Float('TauntID', 0)
|
||||
elseif CLIENT and IsValid(ply.TauntProp) then
|
||||
ply.TauntProp:Remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
706
garrysmod/gamemodes/militaryrp/plugins/arsenal/cl_plugin.lua
Normal file
706
garrysmod/gamemodes/militaryrp/plugins/arsenal/cl_plugin.lua
Normal file
@@ -0,0 +1,706 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
net.Receive("ixArsenalOpen", function()
|
||||
local supply = net.ReadUInt(32)
|
||||
local factionID = net.ReadUInt(8)
|
||||
local weaponsData = net.ReadTable()
|
||||
local weaponHas = net.ReadTable()
|
||||
local armorData = net.ReadTable()
|
||||
local freeRemain = net.ReadUInt(32)
|
||||
|
||||
if not weaponsData or type(weaponsData) ~= "table" then weaponsData = {} end
|
||||
if not weaponHas or type(weaponHas) ~= "table" then weaponHas = {} end
|
||||
|
||||
if IsValid(PLUGIN.menu) then
|
||||
PLUGIN.menu:Remove()
|
||||
end
|
||||
|
||||
local scrW, scrH = ScrW(), ScrH()
|
||||
local frame = vgui.Create("DFrame")
|
||||
frame:SetSize(scrW, scrH)
|
||||
frame:SetPos(0, 0)
|
||||
frame:SetTitle("")
|
||||
frame:SetDraggable(false)
|
||||
frame:ShowCloseButton(false)
|
||||
frame:MakePopup()
|
||||
|
||||
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
|
||||
|
||||
draw.SimpleText("◆ ВОЕННЫЙ АРСЕНАЛ ◆", "ixMenuButtonFont", w/2, 35, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText("СИСТЕМА СНАБЖЕНИЯ", "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(scrW - 80, 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)
|
||||
end
|
||||
|
||||
local supplyBox = vgui.Create("DPanel", infoPanel)
|
||||
supplyBox:SetSize(infoPanel:GetWide() / 2 - 10, infoPanel:GetTall())
|
||||
supplyBox:SetPos(5, 0)
|
||||
supplyBox.Paint = function(s, w, h)
|
||||
draw.SimpleText("ОЧКИ СНАБЖЕНИЯ", "ixSmallFont", 20, 12, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||
draw.SimpleText(tostring(supply), "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 currencySymbol = "$"
|
||||
if PLUGIN and PLUGIN.config and PLUGIN.config.currencySymbol and factionID then
|
||||
currencySymbol = PLUGIN.config.currencySymbol[factionID] or currencySymbol
|
||||
end
|
||||
|
||||
local function formatTime(seconds)
|
||||
seconds = math.max(0, tonumber(seconds) or 0)
|
||||
if seconds <= 0 then return "Готово" end
|
||||
local h = math.floor(seconds / 3600)
|
||||
local m = math.floor((seconds % 3600) / 60)
|
||||
local s = seconds % 60
|
||||
if h > 0 then
|
||||
return string.format("%02d:%02d:%02d", h, m, s)
|
||||
else
|
||||
return string.format("%02d:%02d", m, s)
|
||||
end
|
||||
end
|
||||
|
||||
local cooldownBox = vgui.Create("DPanel", infoPanel)
|
||||
cooldownBox:SetSize(infoPanel:GetWide() / 2 - 10, infoPanel:GetTall())
|
||||
cooldownBox:SetPos(infoPanel:GetWide() / 2 + 5, 0)
|
||||
cooldownBox.Paint = function(s, w, h)
|
||||
draw.SimpleText("БЕСПЛАТНОЕ СНАРЯЖЕНИЕ", "ixSmallFont", 20, 12, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||
|
||||
local timeText = formatTime(freeRemain)
|
||||
local timeColor = freeRemain <= 0 and COLOR_PRIMARY or COLOR_WARNING
|
||||
draw.SimpleText(timeText, "ixMenuButtonFont", 20, 32, timeColor, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP)
|
||||
|
||||
if freeRemain > 0 then
|
||||
surface.SetDrawColor(COLOR_WARNING)
|
||||
draw.NoTexture()
|
||||
surface.DrawCircle(w - 35, h/2, 12, COLOR_WARNING)
|
||||
else
|
||||
surface.SetDrawColor(COLOR_PRIMARY)
|
||||
draw.NoTexture()
|
||||
surface.DrawCircle(w - 35, h/2, 12, COLOR_PRIMARY)
|
||||
end
|
||||
end
|
||||
|
||||
local colWidth = (scrW - 120) / 2
|
||||
local startY = 205
|
||||
|
||||
-- Панель оружия
|
||||
local weaponsPanel = vgui.Create("DPanel", frame)
|
||||
weaponsPanel:SetSize(colWidth, scrH - startY - 40)
|
||||
weaponsPanel:SetPos(40, startY)
|
||||
weaponsPanel.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 weaponsHeader = vgui.Create("DPanel", weaponsPanel)
|
||||
weaponsHeader:SetSize(weaponsPanel:GetWide(), 45)
|
||||
weaponsHeader:SetPos(0, 0)
|
||||
weaponsHeader.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 weaponsScroll = vgui.Create("DScrollPanel", weaponsPanel)
|
||||
weaponsScroll:SetSize(weaponsPanel:GetWide() - 20, weaponsPanel:GetTall() - 60)
|
||||
weaponsScroll:SetPos(10, 50)
|
||||
|
||||
local sbar = weaponsScroll: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 function OpenCategoryWeapons(categoryName, categoryIcon, categoryWeapons)
|
||||
local catFrame = vgui.Create("DFrame")
|
||||
catFrame:SetSize(800, 600)
|
||||
catFrame:Center()
|
||||
catFrame:SetTitle("")
|
||||
catFrame:SetDraggable(true)
|
||||
catFrame:ShowCloseButton(false)
|
||||
catFrame:MakePopup()
|
||||
|
||||
catFrame.Paint = function(s, w, h)
|
||||
draw.RoundedBox(8, 0, 0, w, h, COLOR_BG_DARK)
|
||||
surface.SetDrawColor(COLOR_PRIMARY)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 3)
|
||||
|
||||
-- Заголовок
|
||||
draw.RoundedBoxEx(8, 0, 0, w, 60, COLOR_PRIMARY_DARK, true, true, false, false)
|
||||
draw.SimpleText(categoryIcon .. " " .. categoryName, "ixMenuButtonFont", w/2, 30, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(0, 60, w, 2)
|
||||
end
|
||||
|
||||
-- Кнопка закрытия
|
||||
local catCloseBtn = vgui.Create("DButton", catFrame)
|
||||
catCloseBtn:SetText("")
|
||||
catCloseBtn:SetSize(35, 35)
|
||||
catCloseBtn:SetPos(catFrame:GetWide() - 45, 12)
|
||||
catCloseBtn.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_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)
|
||||
end
|
||||
catCloseBtn.DoClick = function() catFrame:Close() end
|
||||
|
||||
-- Скролл с оружием
|
||||
local catScroll = vgui.Create("DScrollPanel", catFrame)
|
||||
catScroll:SetSize(catFrame:GetWide() - 30, catFrame:GetTall() - 85)
|
||||
catScroll:SetPos(15, 70)
|
||||
|
||||
local catSbar = catScroll:GetVBar()
|
||||
catSbar:SetHideButtons(true)
|
||||
function catSbar:Paint(w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_DARK)
|
||||
end
|
||||
function catSbar.btnGrip:Paint(w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY)
|
||||
end
|
||||
|
||||
-- Создаем карточки оружия
|
||||
for class, data in SortedPairsByMemberValue(categoryWeapons, "name", true) do
|
||||
local has = weaponHas[class]
|
||||
local row = vgui.Create("DPanel", catScroll)
|
||||
row:SetSize(catScroll:GetWide() - 10, 110)
|
||||
row:Dock(TOP)
|
||||
row:DockMargin(0, 0, 0, 10)
|
||||
|
||||
local isHovered = false
|
||||
row.Paint = function(s, w, h)
|
||||
local bgColor = has and Color(45, 25, 20) or 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)
|
||||
|
||||
local statusColor = has and Color(205, 127, 50) or COLOR_PRIMARY
|
||||
draw.RoundedBoxEx(6, 0, 0, 5, h, statusColor, true, false, true, false)
|
||||
|
||||
surface.SetDrawColor(has and Color(205, 127, 50, 80) 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
|
||||
end
|
||||
|
||||
row.OnCursorEntered = function() isHovered = true end
|
||||
row.OnCursorExited = function() isHovered = false end
|
||||
|
||||
-- Название
|
||||
local nameLabel = vgui.Create("DLabel", row)
|
||||
nameLabel:SetText(data.name or class)
|
||||
nameLabel:SetFont("ixSmallFont")
|
||||
nameLabel:SetTextColor(COLOR_TEXT_PRIMARY)
|
||||
nameLabel:SetPos(15, 10)
|
||||
nameLabel:SizeToContents()
|
||||
|
||||
-- Цена
|
||||
local money = data.moneyPrice or 0
|
||||
local supply = data.supplyPrice or 0
|
||||
local priceLabel = vgui.Create("DLabel", row)
|
||||
local priceText = ""
|
||||
if (money or 0) <= 0 and (supply or 0) <= 0 then
|
||||
priceText = "★ БЕСПЛАТНО"
|
||||
else
|
||||
local parts = {}
|
||||
if money > 0 then table.insert(parts, currencySymbol .. tostring(money)) end
|
||||
if supply > 0 then table.insert(parts, tostring(supply) .. " ОС") end
|
||||
priceText = table.concat(parts, " • ")
|
||||
end
|
||||
priceLabel:SetText(priceText)
|
||||
priceLabel:SetFont("ixSmallFont")
|
||||
priceLabel:SetTextColor(money <= 0 and supply <= 0 and COLOR_ACCENT or COLOR_TEXT_PRIMARY)
|
||||
priceLabel:SetPos(15, 40)
|
||||
priceLabel:SizeToContents()
|
||||
|
||||
-- Донат метка
|
||||
if data.donate then
|
||||
local donateLabel = vgui.Create("DLabel", row)
|
||||
donateLabel:SetText("★ ПРЕМИУМ")
|
||||
donateLabel:SetFont("ixSmallFont")
|
||||
donateLabel:SetTextColor(COLOR_WARNING)
|
||||
donateLabel:SetPos(15, 70)
|
||||
donateLabel:SizeToContents()
|
||||
end
|
||||
|
||||
-- Кнопка действия
|
||||
local btn = vgui.Create("DButton", row)
|
||||
btn:SetSize(120, 40)
|
||||
btn:SetPos(row:GetWide() - 130, 35)
|
||||
btn:SetText("")
|
||||
|
||||
local btnText = has and "◄ ВЕРНУТЬ" or "ВЗЯТЬ ►"
|
||||
local btnColor = has and Color(165, 85, 60) or COLOR_PRIMARY
|
||||
local btnColorHover = has and Color(205, 105, 70) or COLOR_ACCENT
|
||||
|
||||
btn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and btnColorHover or btnColor
|
||||
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(btnText, "ixSmallFont", w/2, h/2, COLOR_TEXT_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
btn.DoClick = function()
|
||||
if has then
|
||||
Derma_Query("Вернуть снаряжение? Фракция получит 80% стоимости.", "Возврат", "Подтвердить", function()
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("return_weapon")
|
||||
net.WriteString(class)
|
||||
net.SendToServer()
|
||||
catFrame:Close()
|
||||
frame:Close()
|
||||
end, "Отмена", function() end)
|
||||
else
|
||||
local options = {}
|
||||
if (data.moneyPrice or 0) > 0 then table.insert(options, {text = "Купить за деньги", method = "money"}) end
|
||||
if (data.supplyPrice or 0) >= 0 then table.insert(options, {text = "Купить за очки", method = "supply"}) end
|
||||
|
||||
if #options == 0 then
|
||||
Derma_Message("Невозможно приобрести это оружие", "Ошибка", "OK")
|
||||
return
|
||||
end
|
||||
|
||||
if #options == 1 then
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_weapon")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[1].method)
|
||||
net.SendToServer()
|
||||
catFrame:Close()
|
||||
frame:Close()
|
||||
else
|
||||
local query = Derma_Query("Выберите способ оплаты:", "Оплата", options[1].text, function()
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_weapon")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[1].method)
|
||||
net.SendToServer()
|
||||
catFrame:Close()
|
||||
frame:Close()
|
||||
end, options[2].text, function()
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_weapon")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[2].method)
|
||||
net.SendToServer()
|
||||
catFrame:Close()
|
||||
frame:Close()
|
||||
end, "Отмена", function() end)
|
||||
|
||||
if IsValid(query) then
|
||||
query:ShowCloseButton(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Панель брони
|
||||
local armorPanel = vgui.Create("DPanel", frame)
|
||||
armorPanel:SetSize(colWidth, scrH - startY - 40)
|
||||
armorPanel:SetPos(scrW - colWidth - 40, startY)
|
||||
armorPanel.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 armorHeader = vgui.Create("DPanel", armorPanel)
|
||||
armorHeader:SetSize(armorPanel:GetWide(), 45)
|
||||
armorHeader:SetPos(0, 0)
|
||||
armorHeader.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 armorScroll = vgui.Create("DScrollPanel", armorPanel)
|
||||
armorScroll:SetSize(armorPanel:GetWide() - 20, armorPanel:GetTall() - 60)
|
||||
armorScroll:SetPos(10, 50)
|
||||
|
||||
local sbar2 = armorScroll:GetVBar()
|
||||
sbar2:SetHideButtons(true)
|
||||
function sbar2:Paint(w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_BG_DARK)
|
||||
end
|
||||
function sbar2.btnGrip:Paint(w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, COLOR_PRIMARY)
|
||||
end
|
||||
|
||||
-- Группировка оружия по категориям
|
||||
local categories = {
|
||||
{name = "Основное оружие", icon = "▣", key = "primary", weapons = {}},
|
||||
{name = "Пистолеты", icon = "▢", key = "secondary", weapons = {}},
|
||||
{name = "Гранаты", icon = "◆", key = "grenades", weapons = {}},
|
||||
{name = "Холодное оружие", icon = "▸", key = "melee", weapons = {}},
|
||||
{name = "Прочее", icon = "◇", key = "other", weapons = {}}
|
||||
}
|
||||
|
||||
-- Распределяем оружие по категориям
|
||||
for class, data in pairs(weaponsData) do
|
||||
if type(data) == "table" then
|
||||
local cat = data.category or "other"
|
||||
local added = false
|
||||
|
||||
if cat == "primary" then
|
||||
table.insert(categories[1].weapons, {class = class, data = data})
|
||||
added = true
|
||||
elseif cat == "secondary" then
|
||||
table.insert(categories[2].weapons, {class = class, data = data})
|
||||
added = true
|
||||
elseif cat == "grenade1" or cat == "grenade2" or cat == "grenade" then
|
||||
table.insert(categories[3].weapons, {class = class, data = data})
|
||||
added = true
|
||||
elseif cat == "melee" then
|
||||
table.insert(categories[4].weapons, {class = class, data = data})
|
||||
added = true
|
||||
end
|
||||
|
||||
if not added then
|
||||
table.insert(categories[5].weapons, {class = class, data = data})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Создаем кнопки категорий
|
||||
local totalWeapons = 0
|
||||
for _, category in ipairs(categories) do
|
||||
if #category.weapons > 0 then
|
||||
totalWeapons = totalWeapons + #category.weapons
|
||||
|
||||
local catButton = vgui.Create("DPanel", weaponsScroll)
|
||||
catButton:SetSize(weaponsScroll:GetWide() - 10, 80)
|
||||
catButton:Dock(TOP)
|
||||
catButton:DockMargin(0, 0, 0, 10)
|
||||
catButton:SetCursor("hand")
|
||||
|
||||
local isHovered = false
|
||||
catButton.Paint = function(s, w, h)
|
||||
local bgColor = COLOR_BG_LIGHT
|
||||
if isHovered then
|
||||
bgColor = Color(bgColor.r + 15, bgColor.g + 20, bgColor.b + 15)
|
||||
end
|
||||
draw.RoundedBox(6, 0, 0, w, h, bgColor)
|
||||
|
||||
-- Боковая полоска
|
||||
draw.RoundedBoxEx(6, 0, 0, 6, h, COLOR_PRIMARY, true, false, true, false)
|
||||
|
||||
-- Рамка
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
|
||||
-- Акцентная линия при наведении
|
||||
if isHovered then
|
||||
surface.SetDrawColor(COLOR_ACCENT)
|
||||
surface.DrawRect(6, h-3, w-12, 3)
|
||||
end
|
||||
|
||||
-- Иконка категории
|
||||
draw.SimpleText(category.icon, "ixMenuButtonFont", 30, h/2, COLOR_ACCENT, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Название категории
|
||||
draw.SimpleText(category.name, "ixSmallFont", 60, h/2 - 12, COLOR_TEXT_PRIMARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Количество оружия
|
||||
draw.SimpleText(#category.weapons .. " ед.", "ixSmallFont", 60, h/2 + 12, COLOR_TEXT_SECONDARY, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Стрелка
|
||||
draw.SimpleText("►", "ixSmallFont", w - 25, h/2, COLOR_PRIMARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
catButton.OnCursorEntered = function() isHovered = true end
|
||||
catButton.OnCursorExited = function() isHovered = false end
|
||||
catButton.OnMousePressed = function()
|
||||
-- Подготовка данных для окна категории
|
||||
local categoryWeapons = {}
|
||||
for _, wpn in ipairs(category.weapons) do
|
||||
categoryWeapons[wpn.class] = wpn.data
|
||||
end
|
||||
OpenCategoryWeapons(category.name, category.icon, categoryWeapons)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Если нет оружия вообще
|
||||
if totalWeapons == 0 then
|
||||
local emptyLabel = vgui.Create("DPanel", weaponsScroll)
|
||||
emptyLabel:SetSize(weaponsScroll:GetWide(), 100)
|
||||
emptyLabel:Dock(TOP)
|
||||
emptyLabel:DockMargin(0, 20, 0, 0)
|
||||
emptyLabel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_LIGHT)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
|
||||
draw.SimpleText("⚠ НЕТ ДОСТУПНОГО ОРУЖИЯ", "ixSmallFont", w/2, h/2 - 10, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText("Обратитесь к командованию", "ixSmallFont", w/2, h/2 + 15, Color(COLOR_TEXT_SECONDARY.r * 0.7, COLOR_TEXT_SECONDARY.g * 0.7, COLOR_TEXT_SECONDARY.b * 0.7), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
end
|
||||
|
||||
-- Заполнение брони
|
||||
if not armorData or type(armorData) ~= "table" then armorData = {} end
|
||||
if table.Count(armorData) > 0 then
|
||||
for class, data in SortedPairsByMemberValue(armorData, "name", true) do
|
||||
local row = vgui.Create("DPanel", armorScroll)
|
||||
row:SetSize(armorScroll:GetWide() - 10, 100)
|
||||
row:Dock(TOP)
|
||||
row:DockMargin(0, 0, 0, 10)
|
||||
|
||||
local isHovered = false
|
||||
row.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_ACCENT, true, false, true, false)
|
||||
|
||||
-- Рамка
|
||||
surface.SetDrawColor(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
|
||||
end
|
||||
|
||||
row.OnCursorEntered = function() isHovered = true end
|
||||
row.OnCursorExited = function() isHovered = false end
|
||||
|
||||
-- Название брони
|
||||
local nameLabel = vgui.Create("DLabel", row)
|
||||
nameLabel:SetText(data.name or class)
|
||||
nameLabel:SetFont("ixSmallFont")
|
||||
nameLabel:SetTextColor(COLOR_TEXT_PRIMARY)
|
||||
nameLabel:SetPos(15, 10)
|
||||
nameLabel:SizeToContents()
|
||||
|
||||
-- Параметры брони
|
||||
local amountLabel = vgui.Create("DLabel", row)
|
||||
amountLabel:SetText("⬢ Защита: " .. tostring(data.amount or 0) .. " ед.")
|
||||
amountLabel:SetFont("ixSmallFont")
|
||||
amountLabel:SetTextColor(COLOR_TEXT_SECONDARY)
|
||||
amountLabel:SetPos(15, 35)
|
||||
amountLabel:SizeToContents()
|
||||
|
||||
-- Цена
|
||||
local moneyP = data.moneyPrice or 0
|
||||
local supplyP = data.supplyPrice or 0
|
||||
local priceText = ""
|
||||
if (moneyP or 0) <= 0 and (supplyP or 0) <= 0 then
|
||||
priceText = "★ БЕСПЛАТНО"
|
||||
else
|
||||
local parts = {}
|
||||
if moneyP > 0 then table.insert(parts, currencySymbol .. tostring(moneyP)) end
|
||||
if supplyP > 0 then table.insert(parts, tostring(supplyP) .. " ОС") end
|
||||
priceText = table.concat(parts, " • ")
|
||||
end
|
||||
local priceLabel = vgui.Create("DLabel", row)
|
||||
priceLabel:SetText(priceText)
|
||||
priceLabel:SetFont("ixSmallFont")
|
||||
priceLabel:SetTextColor(moneyP <= 0 and supplyP <= 0 and COLOR_ACCENT or COLOR_TEXT_PRIMARY)
|
||||
priceLabel:SetPos(15, 58)
|
||||
priceLabel:SizeToContents()
|
||||
|
||||
-- Кнопка покупки
|
||||
local btn = vgui.Create("DButton", row)
|
||||
btn:SetSize(120, 40)
|
||||
btn:SetPos(row:GetWide() - 130, 30)
|
||||
btn:SetText("")
|
||||
|
||||
btn.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
|
||||
|
||||
btn.DoClick = function()
|
||||
local options = {}
|
||||
if (data.moneyPrice or 0) > 0 then table.insert(options, {text = "Купить за деньги", method = "money"}) end
|
||||
if (data.supplyPrice or 0) >= 0 then table.insert(options, {text = "Купить за очки", method = "supply"}) end
|
||||
|
||||
if #options == 0 then
|
||||
Derma_Message("Невозможно приобрести эту броню", "Ошибка", "OK")
|
||||
return
|
||||
end
|
||||
|
||||
if #options == 1 then
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_armor")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[1].method)
|
||||
net.SendToServer()
|
||||
frame:Close()
|
||||
else
|
||||
local query = Derma_Query("Выберите способ оплаты:", "Оплата", options[1].text, function()
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_armor")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[1].method)
|
||||
net.SendToServer()
|
||||
frame:Close()
|
||||
end, options[2].text, function()
|
||||
net.Start("ixArsenalAction")
|
||||
net.WriteString("buy_armor")
|
||||
net.WriteString(class)
|
||||
net.WriteString(options[2].method)
|
||||
net.SendToServer()
|
||||
frame:Close()
|
||||
end, "Отмена", function() end)
|
||||
|
||||
if IsValid(query) then
|
||||
query:ShowCloseButton(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local emptyLabel = vgui.Create("DPanel", armorScroll)
|
||||
emptyLabel:SetSize(armorScroll:GetWide(), 100)
|
||||
emptyLabel:Dock(TOP)
|
||||
emptyLabel:DockMargin(0, 20, 0, 0)
|
||||
emptyLabel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, COLOR_BG_LIGHT)
|
||||
surface.SetDrawColor(COLOR_BORDER)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
|
||||
draw.SimpleText("⚠ НЕТ ДОСТУПНОЙ ЗАЩИТЫ", "ixSmallFont", w/2, h/2 - 10, COLOR_TEXT_SECONDARY, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText("Обратитесь к командованию", "ixSmallFont", w/2, h/2 + 15, Color(COLOR_TEXT_SECONDARY.r * 0.7, COLOR_TEXT_SECONDARY.g * 0.7, COLOR_TEXT_SECONDARY.b * 0.7), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
end
|
||||
|
||||
PLUGIN.menu = frame
|
||||
end)
|
||||
@@ -0,0 +1,234 @@
|
||||
AddCSLuaFile()
|
||||
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
ENT.PrintName = "Ящик с патронами"
|
||||
ENT.Author = "Server"
|
||||
ENT.Spawnable = true
|
||||
ENT.AdminOnly = true
|
||||
ENT.Category = "[FT] Система арсенала"
|
||||
ENT.bNoPersist = true
|
||||
|
||||
if SERVER then
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/trenches/prop/para_ammo/ammo_supply.mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
|
||||
local phys = self:GetPhysicsObject()
|
||||
if IsValid(phys) then
|
||||
phys:Wake()
|
||||
end
|
||||
|
||||
-- Таблица для хранения кулдаунов игроков
|
||||
self.playerCooldowns = {}
|
||||
end
|
||||
|
||||
-- Получить информацию о патронах для оружия
|
||||
function ENT:GetWeaponAmmoInfo(weapon)
|
||||
if not IsValid(weapon) then return nil end
|
||||
|
||||
local ammoType = weapon:GetPrimaryAmmoType()
|
||||
if ammoType == -1 then return nil end
|
||||
|
||||
local clipSize = weapon:GetMaxClip1()
|
||||
if clipSize <= 0 then return nil end
|
||||
|
||||
return {
|
||||
ammoType = ammoType,
|
||||
clipSize = clipSize,
|
||||
ammoName = game.GetAmmoName(ammoType) or "unknown"
|
||||
}
|
||||
end
|
||||
|
||||
-- Использование ящика
|
||||
function ENT:Use(activator, caller)
|
||||
if not IsValid(activator) or not activator:IsPlayer() then return end
|
||||
|
||||
-- Проверка кулдауна (1 секунда)
|
||||
local steamID = activator:SteamID()
|
||||
local now = CurTime()
|
||||
if self.playerCooldowns[steamID] and (now - self.playerCooldowns[steamID]) < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local char = activator:GetCharacter()
|
||||
if not char then
|
||||
activator:Notify("У вас нет активного персонажа")
|
||||
return
|
||||
end
|
||||
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then
|
||||
activator:Notify("Плагин арсенала не найден")
|
||||
return
|
||||
end
|
||||
|
||||
-- Проверка оружия в руках
|
||||
local weapon = activator:GetActiveWeapon()
|
||||
if not IsValid(weapon) then
|
||||
activator:Notify("Возьмите оружие в руки")
|
||||
return
|
||||
end
|
||||
|
||||
-- Получение информации о патронах
|
||||
local ammoInfo = self:GetWeaponAmmoInfo(weapon)
|
||||
local isMedicalKit = weapon:GetClass() == "tacrp_ifak"
|
||||
|
||||
if not ammoInfo and not isMedicalKit then
|
||||
activator:Notify("Это оружие не использует патроны")
|
||||
return
|
||||
end
|
||||
|
||||
-- Специальная логика для медицинской аптечки IFAK
|
||||
if isMedicalKit then
|
||||
-- Проверяем текущие запасы медицинских расходников
|
||||
local bandages = activator:GetAmmoCount(game.GetAmmoID("Bandages"))
|
||||
local quikclots = activator:GetAmmoCount(game.GetAmmoID("Quikclots"))
|
||||
local hemostats = activator:GetAmmoCount(game.GetAmmoID("Hemostats"))
|
||||
|
||||
local maxBandages = 4
|
||||
local maxQuikclots = 3
|
||||
local maxHemostats = 2
|
||||
|
||||
if bandages >= maxBandages and quikclots >= maxQuikclots and hemostats >= maxHemostats then
|
||||
activator:Notify("У вас уже полный комплект медицинских расходников")
|
||||
return
|
||||
end
|
||||
|
||||
-- Рассчитываем стоимость пополнения
|
||||
local costPerItem = 5 -- стоимость за единицу
|
||||
local totalCost = 0
|
||||
local itemsToGive = {}
|
||||
|
||||
if bandages < maxBandages then
|
||||
local needed = maxBandages - bandages
|
||||
totalCost = totalCost + needed * costPerItem
|
||||
itemsToGive["Bandages"] = needed
|
||||
end
|
||||
|
||||
if quikclots < maxQuikclots then
|
||||
local needed = maxQuikclots - quikclots
|
||||
totalCost = totalCost + needed * costPerItem
|
||||
itemsToGive["Quikclots"] = needed
|
||||
end
|
||||
|
||||
if hemostats < maxHemostats then
|
||||
local needed = maxHemostats - hemostats
|
||||
totalCost = totalCost + needed * costPerItem
|
||||
itemsToGive["Hemostats"] = needed
|
||||
end
|
||||
|
||||
-- Проверка достаточно ли очков снабжения
|
||||
if supply < totalCost then
|
||||
activator:Notify(string.format("Недостаточно очков снабжения. Требуется: %d, доступно: %d", totalCost, supply))
|
||||
return
|
||||
end
|
||||
|
||||
-- Выдаем расходники
|
||||
for ammoType, amount in pairs(itemsToGive) do
|
||||
activator:GiveAmmo(amount, ammoType, true)
|
||||
end
|
||||
|
||||
-- Списываем очки снабжения
|
||||
plugin:AddFactionSupply(factionID, -totalCost)
|
||||
|
||||
-- Устанавливаем кулдаун
|
||||
self.playerCooldowns[steamID] = now
|
||||
|
||||
-- Уведомление
|
||||
local itemList = {}
|
||||
for ammoType, amount in pairs(itemsToGive) do
|
||||
table.insert(itemList, string.format("%s: +%d", ammoType, amount))
|
||||
end
|
||||
activator:Notify(string.format("Получены медицинские расходники: %s", table.concat(itemList, ", ")))
|
||||
|
||||
-- Звуковой эффект
|
||||
self:EmitSound("items/ammo_pickup.wav")
|
||||
|
||||
-- Логирование
|
||||
print(string.format("[AMMOBOX] %s получил медицинские расходники. Стоимость: %d очков",
|
||||
activator:Nick(), totalCost))
|
||||
return
|
||||
end
|
||||
|
||||
-- Проверяем конфиг арсенала для этого оружия (одноразовые гранатомёты)
|
||||
local weaponClass = weapon:GetClass()
|
||||
local arsenalConfig = plugin.config and plugin.config.weapons and plugin.config.weapons[weaponClass]
|
||||
-- Детект по конфигу ИЛИ по типу патронов — rpg_round и PanzerFaust3 Rocket всегда одноразовые
|
||||
local launcherAmmoTypes = { ["rpg_round"] = true, ["PanzerFaust3 Rocket"] = true }
|
||||
local isOneShotLauncher = (arsenalConfig and (arsenalConfig.maxAmmo == 1 or arsenalConfig.maxCount == 1))
|
||||
or launcherAmmoTypes[ammoInfo.ammoName]
|
||||
|
||||
-- Проверка фракции
|
||||
local factionID = char:GetFaction()
|
||||
local supply = plugin:GetFactionSupply(factionID)
|
||||
supply = tonumber(supply) or 0
|
||||
|
||||
if isOneShotLauncher then
|
||||
-- Одноразовый гранатомёт: max 1 снаряд в запасе
|
||||
local currentAmmo = activator:GetAmmoCount(ammoInfo.ammoType)
|
||||
if currentAmmo >= 1 then
|
||||
activator:Notify("У вас уже есть снаряд для этого оружия")
|
||||
return
|
||||
end
|
||||
|
||||
-- Стоимость 1 снаряда через costPerBullet
|
||||
local totalCost = 1
|
||||
if supply < totalCost then
|
||||
activator:Notify(string.format("Недостаточно очков снабжения. Требуется: %d, доступно: %d", totalCost, supply))
|
||||
return
|
||||
end
|
||||
|
||||
-- Выдаём ровно 1 снаряд через SetAmmo (GiveAmmo выдаёт минимум 6 для rpg_round)
|
||||
activator:SetAmmo(currentAmmo + 1, ammoInfo.ammoType)
|
||||
plugin:AddFactionSupply(factionID, -totalCost)
|
||||
self.playerCooldowns[steamID] = now
|
||||
|
||||
activator:Notify("Получен 1 снаряд")
|
||||
self:EmitSound("items/ammo_pickup.wav")
|
||||
else
|
||||
-- Обычное оружие: проверка реального количества патронов у игрока
|
||||
local currentAmmo = activator:GetAmmoCount(ammoInfo.ammoType)
|
||||
local maxAllowedAmmo = ammoInfo.clipSize * 6 -- 6 магазинов запаса
|
||||
|
||||
if currentAmmo >= maxAllowedAmmo then
|
||||
local currentMags = math.floor(currentAmmo / ammoInfo.clipSize)
|
||||
activator:Notify(string.format("У вас уже максимум патронов (%d магазинов, 6/6)", currentMags))
|
||||
return
|
||||
end
|
||||
|
||||
-- Рассчитываем количество патронов (1 магазин)
|
||||
local totalAmmo = ammoInfo.clipSize
|
||||
local costPerBullet = 1
|
||||
local totalCost = totalAmmo * costPerBullet
|
||||
|
||||
if supply < totalCost then
|
||||
activator:Notify(string.format("Недостаточно очков снабжения. Требуется: %d, доступно: %d", totalCost, supply))
|
||||
return
|
||||
end
|
||||
|
||||
-- Выдаем патроны
|
||||
activator:GiveAmmo(totalAmmo, ammoInfo.ammoType, true)
|
||||
plugin:AddFactionSupply(factionID, -totalCost)
|
||||
self.playerCooldowns[steamID] = now
|
||||
|
||||
-- Подсчёт текущих магазинов после выдачи
|
||||
local newAmmo = activator:GetAmmoCount(ammoInfo.ammoType)
|
||||
local newMags = math.floor(newAmmo / ammoInfo.clipSize)
|
||||
local magsStr = math.min(newMags, 6) .. "/6"
|
||||
activator:Notify(string.format("Получено %d %s (магазинов: %s)", totalAmmo,
|
||||
totalAmmo == 1 and "патрон" or (totalAmmo < 5 and "патрона" or "патронов"), magsStr))
|
||||
self:EmitSound("items/ammo_pickup.wav")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if CLIENT then
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
AddCSLuaFile()
|
||||
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
ENT.PrintName = "Арсенал"
|
||||
ENT.Author = "Server"
|
||||
ENT.Spawnable = true
|
||||
ENT.AdminOnly = true
|
||||
ENT.Category = "[FT] Система арсенала"
|
||||
ENT.bNoPersist = true
|
||||
|
||||
if SERVER then
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/ft/shkaf.mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
local phys = self:GetPhysicsObject()
|
||||
if IsValid(phys) then phys:Wake() end
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
if not IsValid(activator) or not activator:IsPlayer() then return end
|
||||
local char = activator:GetCharacter()
|
||||
if not char then return end
|
||||
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then return end
|
||||
|
||||
local avail = plugin:GetAvailableWeapons(char)
|
||||
local weaponsData = {}
|
||||
local weaponHas = {}
|
||||
for class, data in pairs(avail) do
|
||||
weaponsData[class] = data
|
||||
weaponHas[class] = false
|
||||
end
|
||||
|
||||
for _, wep in ipairs(activator:GetWeapons()) do
|
||||
if IsValid(wep) then
|
||||
local c = wep:GetClass()
|
||||
if weaponHas[c] ~= nil then weaponHas[c] = true end
|
||||
end
|
||||
end
|
||||
|
||||
local supply = plugin:GetFactionSupply(char:GetFaction())
|
||||
supply = tonumber(supply) or 0
|
||||
|
||||
local freeRemain = plugin:GetFreeWeaponCooldownForPlayer(activator)
|
||||
local factionID = char:GetFaction()
|
||||
net.Start("ixArsenalOpen")
|
||||
net.WriteUInt(supply, 32)
|
||||
net.WriteUInt(factionID or 0, 8)
|
||||
net.WriteTable(weaponsData)
|
||||
net.WriteTable(weaponHas)
|
||||
net.WriteTable(plugin.config.armor or {})
|
||||
net.WriteUInt(math.max(0, tonumber(freeRemain) or 0), 32)
|
||||
net.Send(activator)
|
||||
end
|
||||
end
|
||||
|
||||
if CLIENT then
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
--local pos = self:GetPos() + Vector(0,0,10)
|
||||
--local ang = Angle(0, LocalPlayer():EyeAngles().y - 90, 90)
|
||||
--cam.Start3D2D(pos, ang, 0.1)
|
||||
-- draw.SimpleText("АРСЕНАЛ", "DermaDefaultBold", 0, 0, Color(200,200,200), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
--cam.End3D2D()
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,596 @@
|
||||
AddCSLuaFile()
|
||||
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
ENT.PrintName = "Гардероб"
|
||||
ENT.Author = "Server"
|
||||
ENT.Spawnable = true
|
||||
ENT.AdminOnly = true
|
||||
ENT.Category = "[FT] Система арсенала"
|
||||
ENT.bNoPersist = true
|
||||
|
||||
if SERVER then
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/props_wasteland/controlroom_storagecloset001a.mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
local phys = self:GetPhysicsObject()
|
||||
if IsValid(phys) then phys:Wake() end
|
||||
self.playerCooldowns = {}
|
||||
end
|
||||
|
||||
function ENT:GetModelBodygroups(model)
|
||||
local tempEnt = ents.Create("prop_dynamic")
|
||||
if not IsValid(tempEnt) then return {} end
|
||||
tempEnt:SetModel(model)
|
||||
tempEnt:Spawn()
|
||||
local bodygroups = {}
|
||||
for i = 0, tempEnt:GetNumBodyGroups() - 1 do
|
||||
local name = tempEnt:GetBodygroupName(i)
|
||||
local count = tempEnt:GetBodygroupCount(i)
|
||||
if count > 1 then
|
||||
bodygroups[i] = {name = name, count = count, index = i}
|
||||
end
|
||||
end
|
||||
tempEnt:Remove()
|
||||
return bodygroups
|
||||
end
|
||||
|
||||
function ENT:GetModelSkinCount(model)
|
||||
local tempEnt = ents.Create("prop_dynamic")
|
||||
if not IsValid(tempEnt) then return 0 end
|
||||
tempEnt:SetModel(model)
|
||||
tempEnt:Spawn()
|
||||
local skinCount = tempEnt:SkinCount() or 0
|
||||
tempEnt:Remove()
|
||||
return skinCount
|
||||
end
|
||||
|
||||
function ENT:IsBlacklisted(model, type, index)
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin or not plugin.config.wardrobeBlacklist then return false end
|
||||
local blacklist = plugin.config.wardrobeBlacklist[model]
|
||||
if not blacklist then return false end
|
||||
if type == "bodygroup" and blacklist.bodygroups then
|
||||
return table.HasValue(blacklist.bodygroups, index)
|
||||
elseif type == "skin" and blacklist.skins then
|
||||
return table.HasValue(blacklist.skins, index)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ENT:Use(activator)
|
||||
if not IsValid(activator) or not activator:IsPlayer() then return end
|
||||
if not self.playerCooldowns then self.playerCooldowns = {} end
|
||||
local steamID = activator:SteamID()
|
||||
local now = CurTime()
|
||||
if self.playerCooldowns[steamID] and (now - self.playerCooldowns[steamID]) < 2 then return end
|
||||
local char = activator:GetCharacter()
|
||||
if not char then return end
|
||||
local model = activator:GetModel()
|
||||
if not model then return end
|
||||
|
||||
local bodygroups = self:GetModelBodygroups(model)
|
||||
local skinCount = self:GetModelSkinCount(model)
|
||||
local availableBodygroups = {}
|
||||
for idx, data in pairs(bodygroups) do
|
||||
if not self:IsBlacklisted(model, "bodygroup", idx) then
|
||||
availableBodygroups[idx] = data
|
||||
end
|
||||
end
|
||||
local availableSkins = {}
|
||||
for i = 0, skinCount - 1 do
|
||||
if not self:IsBlacklisted(model, "skin", i) then
|
||||
table.insert(availableSkins, i)
|
||||
end
|
||||
end
|
||||
|
||||
local currentBodygroups = {}
|
||||
for idx in pairs(availableBodygroups) do
|
||||
currentBodygroups[idx] = activator:GetBodygroup(idx)
|
||||
end
|
||||
local currentSkin = activator:GetSkin()
|
||||
local currentPatch = char:GetData("patchIndex", 1)
|
||||
|
||||
self.playerCooldowns[steamID] = now
|
||||
net.Start("ixWardrobeOpen")
|
||||
net.WriteString(model)
|
||||
net.WriteTable(availableBodygroups)
|
||||
net.WriteTable(availableSkins)
|
||||
net.WriteTable(currentBodygroups)
|
||||
net.WriteUInt(currentSkin, 8)
|
||||
net.WriteUInt(currentPatch, 8)
|
||||
net.Send(activator)
|
||||
end
|
||||
|
||||
net.Receive("ixWardrobeApply", function(len, client)
|
||||
if not IsValid(client) then return end
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
|
||||
local isWipe = net.ReadBool()
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
|
||||
if isWipe then
|
||||
client:SetSkin(0)
|
||||
char:SetData("skin", 0)
|
||||
char:SetData("patchIndex", 1)
|
||||
char:SetData("bodygroups", nil)
|
||||
|
||||
local bgs = client:GetBodyGroups()
|
||||
for _, bg in ipairs(bgs) do
|
||||
client:SetBodygroup(bg.id, 0)
|
||||
end
|
||||
|
||||
if plugin and plugin.config.patchIndex then
|
||||
client:SetSubMaterial(plugin.config.patchIndex, "")
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local bodygroups = net.ReadTable()
|
||||
local skin = net.ReadUInt(8)
|
||||
local patch = net.ReadUInt(8)
|
||||
local model = client:GetModel()
|
||||
|
||||
local faction = char:GetFaction()
|
||||
for idx, value in pairs(bodygroups) do
|
||||
if plugin and plugin.config.wardrobeAccess and plugin.config.wardrobeAccess[model] then
|
||||
local bAccess = plugin.config.wardrobeAccess[model][idx]
|
||||
if bAccess and bAccess[value] then
|
||||
if not table.HasValue(bAccess[value], faction) then
|
||||
value = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
client:SetBodygroup(idx, value)
|
||||
end
|
||||
|
||||
client:SetSkin(skin)
|
||||
char:SetData("bodygroups", bodygroups)
|
||||
char:SetData("skin", skin)
|
||||
char:SetData("patchIndex", patch)
|
||||
|
||||
if plugin and plugin.config.patchIndex and plugin.config.patchMaterials then
|
||||
local pMat = plugin.config.patchMaterials[patch]
|
||||
if pMat then
|
||||
client:SetSubMaterial(plugin.config.patchIndex, pMat)
|
||||
else
|
||||
client:SetSubMaterial(plugin.config.patchIndex, "")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if CLIENT then
|
||||
local C_BG_D = Color(3, 5, 4)
|
||||
local C_BG_M = Color(8, 12, 10)
|
||||
local C_BG_L = Color(12, 18, 14)
|
||||
local C_PRI = Color(27, 94, 32)
|
||||
local C_PRI_H = Color(33, 110, 38)
|
||||
local C_ACC = Color(56, 102, 35)
|
||||
local C_ACC_H = Color(66, 120, 45)
|
||||
local C_BDR = Color(46, 125, 50, 80)
|
||||
local C_TXT_P = Color(165, 214, 167)
|
||||
local C_TXT_S = Color(129, 199, 132)
|
||||
local C_GRAD = Color(27, 94, 32, 15)
|
||||
local C_WARN = Color(150, 40, 40)
|
||||
local C_WARN_H = Color(180, 50, 50)
|
||||
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
end
|
||||
|
||||
net.Receive("ixWardrobeOpen", function()
|
||||
local model = net.ReadString()
|
||||
local bodygroups = net.ReadTable()
|
||||
local availableSkins = net.ReadTable()
|
||||
local currentBodygroups = net.ReadTable()
|
||||
local currentSkin = net.ReadUInt(8)
|
||||
local currentPatch = net.ReadUInt(8)
|
||||
if IsRecruit(ply) then
|
||||
ply:Notify("Новоприбывший не имеет доступа к гардеробу.")
|
||||
return
|
||||
end
|
||||
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then return end
|
||||
|
||||
local frame = vgui.Create("DFrame")
|
||||
frame:SetSize(900, 600)
|
||||
frame:Center()
|
||||
frame:SetTitle("")
|
||||
frame:ShowCloseButton(false)
|
||||
frame:MakePopup()
|
||||
frame:SetBackgroundBlur(true)
|
||||
frame.Paint = function(s, w, h)
|
||||
draw.RoundedBox(8, 0, 0, w, h, C_BG_D)
|
||||
surface.SetDrawColor(C_GRAD)
|
||||
surface.DrawRect(0, 0, w, 3)
|
||||
draw.RoundedBoxEx(8, 0, 0, w, 50, C_BG_M, true, true, false, false)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawLine(0, 50, w, 50)
|
||||
draw.SimpleText("ГАРДЕРОБ", "DermaLarge", w/2, 25, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
surface.SetDrawColor(C_ACC)
|
||||
surface.DrawRect(20, 48, 30, 2)
|
||||
surface.DrawRect(w-50, 48, 30, 2)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local closeBtn = vgui.Create("DButton", frame)
|
||||
closeBtn:SetPos(frame:GetWide() - 40, 10)
|
||||
closeBtn:SetSize(30, 30)
|
||||
closeBtn:SetText("")
|
||||
closeBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_ACC_H or C_ACC
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
draw.SimpleText("X", "DermaLarge", w/2, h/2-2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
closeBtn.DoClick = function() frame:Close() end
|
||||
|
||||
local modelPanel = vgui.Create("DModelPanel", frame)
|
||||
modelPanel:SetPos(20, 60)
|
||||
modelPanel:SetSize(400, 460)
|
||||
modelPanel:SetModel(model)
|
||||
|
||||
local oldPaint = modelPanel.Paint
|
||||
modelPanel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, C_BG_M)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
oldPaint(s, w, h)
|
||||
end
|
||||
|
||||
modelPanel.LayoutEntity = function(s, ent)
|
||||
if not s.bAnimSet then
|
||||
local seq = ent:LookupSequence("idle_subtle")
|
||||
if seq <= 0 then seq = ent:LookupSequence("idle_all_01") end
|
||||
if seq > 0 then
|
||||
ent:SetSequence(seq)
|
||||
end
|
||||
s.bAnimSet = true
|
||||
end
|
||||
|
||||
if (s.bDragging) then
|
||||
local x, y = gui.MousePos()
|
||||
s.camAnims.rot.yaw = s.camAnims.rot.yaw + (x - s.lastX) * 0.8
|
||||
s.camAnims.rot.pitch = math.Clamp(s.camAnims.rot.pitch + (y - s.lastY) * 0.8, -45, 45)
|
||||
s.lastX, s.lastY = x, y
|
||||
end
|
||||
|
||||
local speed = FrameTime() * 10
|
||||
s.curZoom = Lerp(speed, s.curZoom, s.camAnims.zoom)
|
||||
s.curRot = LerpAngle(speed, s.curRot, s.camAnims.rot)
|
||||
|
||||
local eyePos = ent:GetPos() + Vector(0, 0, 40)
|
||||
s:SetCamPos(eyePos - s.curRot:Forward() * s.curZoom)
|
||||
s:SetLookAng(s.curRot)
|
||||
|
||||
ent:FrameAdvance((RealTime() - (s.lastTick or RealTime())) * 1)
|
||||
s.lastTick = RealTime()
|
||||
end
|
||||
|
||||
modelPanel.camAnims = { zoom = 60, rot = Angle(0, 180, 0) }
|
||||
modelPanel.curZoom = 60
|
||||
modelPanel.curRot = Angle(0, 180, 0)
|
||||
|
||||
local function SetCameraAngle(angle)
|
||||
if angle == "front" then
|
||||
modelPanel.camAnims.rot = Angle(0, 180, 0)
|
||||
elseif angle == "side" then
|
||||
modelPanel.camAnims.rot = Angle(0, 90, 0)
|
||||
elseif angle == "back" then
|
||||
modelPanel.camAnims.rot = Angle(0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
modelPanel.OnMousePressed = function(s, code)
|
||||
if code == MOUSE_LEFT then
|
||||
s.bDragging = true
|
||||
s.lastX, s.lastY = gui.MousePos()
|
||||
end
|
||||
end
|
||||
|
||||
modelPanel.OnMouseReleased = function(s, code)
|
||||
if code == MOUSE_LEFT then
|
||||
s.bDragging = false
|
||||
end
|
||||
end
|
||||
|
||||
modelPanel.OnCursorExited = function(s)
|
||||
s.bDragging = false
|
||||
end
|
||||
|
||||
modelPanel.OnMouseWheeled = function(s, delta)
|
||||
s.camAnims.zoom = math.Clamp(s.camAnims.zoom - delta * 15, 20, 120)
|
||||
end
|
||||
|
||||
timer.Simple(0.05, function()
|
||||
if IsValid(modelPanel) then SetCameraAngle("front") end
|
||||
end)
|
||||
|
||||
modelPanel.Think = function(s)
|
||||
local entity = s:GetEntity()
|
||||
if IsValid(entity) then
|
||||
for idx, value in pairs(currentBodygroups) do
|
||||
if entity:GetBodygroup(idx) ~= value then
|
||||
entity:SetBodygroup(idx, value)
|
||||
end
|
||||
end
|
||||
if entity:GetSkin() ~= currentSkin then
|
||||
entity:SetSkin(currentSkin)
|
||||
end
|
||||
|
||||
if plugin and plugin.config.patchIndex and plugin.config.patchMaterials then
|
||||
local pMat = plugin.config.patchMaterials[currentPatch]
|
||||
if pMat then
|
||||
entity:SetSubMaterial(plugin.config.patchIndex, pMat)
|
||||
else
|
||||
entity:SetSubMaterial(plugin.config.patchIndex, "")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local viewButtons = vgui.Create("DPanel", frame)
|
||||
viewButtons:SetPos(20, 530)
|
||||
viewButtons:SetSize(400, 50)
|
||||
viewButtons.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, C_BG_M)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local bw = 125
|
||||
local bs = 12
|
||||
|
||||
local frontBtn = vgui.Create("DButton", viewButtons)
|
||||
frontBtn:SetPos(10, 10)
|
||||
frontBtn:SetSize(bw, 30)
|
||||
frontBtn:SetText("")
|
||||
frontBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_PRI_H or C_PRI
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
draw.SimpleText("СПЕРЕДИ", "DermaDefault", w/2, h/2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
frontBtn.DoClick = function() SetCameraAngle("front") end
|
||||
|
||||
local sideBtn = vgui.Create("DButton", viewButtons)
|
||||
sideBtn:SetPos(10 + bw + bs, 10)
|
||||
sideBtn:SetSize(bw, 30)
|
||||
sideBtn:SetText("")
|
||||
sideBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_PRI_H or C_PRI
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
draw.SimpleText("СБОКУ", "DermaDefault", w/2, h/2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
sideBtn.DoClick = function() SetCameraAngle("side") end
|
||||
|
||||
local backBtn = vgui.Create("DButton", viewButtons)
|
||||
backBtn:SetPos(10 + (bw + bs) * 2, 10)
|
||||
backBtn:SetSize(bw, 30)
|
||||
backBtn:SetText("")
|
||||
backBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_PRI_H or C_PRI
|
||||
draw.RoundedBox(4, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
draw.SimpleText("СЗАДИ", "DermaDefault", w/2, h/2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
backBtn.DoClick = function() SetCameraAngle("back") end
|
||||
|
||||
local settingsPanel = vgui.Create("DPanel", frame)
|
||||
settingsPanel:SetPos(440, 60)
|
||||
settingsPanel:SetSize(440, 520)
|
||||
settingsPanel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(6, 0, 0, w, h, C_BG_M)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local settingsScroll = vgui.Create("DScrollPanel", settingsPanel)
|
||||
settingsScroll:Dock(FILL)
|
||||
settingsScroll:DockMargin(10, 10, 10, 140)
|
||||
|
||||
local sbar = settingsScroll:GetVBar()
|
||||
sbar:SetWide(8)
|
||||
sbar.Paint = function(s, w, h) draw.RoundedBox(4, 0, 0, w, h, C_BG_D) end
|
||||
sbar.btnGrip.Paint = function(s, w, h) draw.RoundedBox(4, 0, 0, w, h, C_ACC) end
|
||||
sbar.btnUp.Paint = function() end
|
||||
sbar.btnDown.Paint = function() end
|
||||
|
||||
local yPos = 5
|
||||
|
||||
if plugin and plugin.config.patchMaterials and #plugin.config.patchMaterials > 0 then
|
||||
local patchLabel = vgui.Create("DPanel", settingsScroll)
|
||||
patchLabel:SetPos(10, yPos)
|
||||
patchLabel:SetSize(400, 35)
|
||||
patchLabel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, C_BG_L)
|
||||
surface.SetDrawColor(C_ACC)
|
||||
surface.DrawRect(0, 0, 3, h)
|
||||
draw.SimpleText("ПАТЧИ", "DermaLarge", 10, h/2, C_TXT_P, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
yPos = yPos + 40
|
||||
|
||||
local pH = vgui.Create("DPanel", settingsScroll)
|
||||
pH:SetPos(10, yPos)
|
||||
pH:SetSize(400, 65)
|
||||
pH.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_BG_L or C_BG_D
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local pS = vgui.Create("DNumSlider", pH)
|
||||
pS:SetPos(5, 15)
|
||||
pS:SetSize(390, 40)
|
||||
pS:SetMin(1)
|
||||
pS:SetMax(#plugin.config.patchMaterials)
|
||||
pS:SetDecimals(0)
|
||||
pS:SetValue(currentPatch)
|
||||
pS:SetText("Тип патча")
|
||||
pS.Slider.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, h/2-2, w, 4, C_BG_D)
|
||||
local maxV = math.max(1, pS:GetMax() - pS:GetMin())
|
||||
local frac = (pS:GetValue() - pS:GetMin()) / maxV
|
||||
draw.RoundedBox(4, 0, h/2-2, w * frac, 4, C_ACC)
|
||||
end
|
||||
pS.Slider.Knob.Paint = function(s, w, h)
|
||||
draw.RoundedBox(w/2, 0, 0, w, h, C_PRI)
|
||||
if s:IsHovered() then draw.RoundedBox(w/2, 2, 2, w-4, h-4, C_ACC_H) end
|
||||
end
|
||||
pS.OnValueChanged = function(s, value) currentPatch = math.floor(value) end
|
||||
|
||||
yPos = yPos + 70
|
||||
end
|
||||
|
||||
if table.Count(bodygroups) > 0 then
|
||||
local bgLabel = vgui.Create("DPanel", settingsScroll)
|
||||
bgLabel:SetPos(10, yPos)
|
||||
bgLabel:SetSize(400, 35)
|
||||
bgLabel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, C_BG_L)
|
||||
surface.SetDrawColor(C_ACC)
|
||||
surface.DrawRect(0, 0, 3, h)
|
||||
draw.SimpleText("ЧАСТИ ТЕЛА", "DermaLarge", 10, h/2, C_TXT_P, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
yPos = yPos + 40
|
||||
|
||||
for idx, data in SortedPairsByMemberValue(bodygroups, "index") do
|
||||
local bgPanel = vgui.Create("DPanel", settingsScroll)
|
||||
bgPanel:SetPos(10, yPos)
|
||||
bgPanel:SetSize(400, 65)
|
||||
bgPanel.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_BG_L or C_BG_D
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local label = vgui.Create("DLabel", bgPanel)
|
||||
label:SetPos(10, 8)
|
||||
label:SetSize(380, 20)
|
||||
label:SetFont("DermaDefault")
|
||||
label:SetText(data.name)
|
||||
label:SetTextColor(C_TXT_P)
|
||||
|
||||
local slider = vgui.Create("DNumSlider", bgPanel)
|
||||
slider:SetPos(5, 28)
|
||||
slider:SetSize(390, 30)
|
||||
slider:SetMin(0)
|
||||
slider:SetMax(data.count - 1)
|
||||
slider:SetDecimals(0)
|
||||
slider:SetValue(currentBodygroups[idx] or 0)
|
||||
slider:SetText("")
|
||||
slider.Slider.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, h/2-2, w, 4, C_BG_D)
|
||||
local maxV = math.max(1, slider:GetMax() - slider:GetMin())
|
||||
local frac = (slider:GetValue() - slider:GetMin()) / maxV
|
||||
draw.RoundedBox(4, 0, h/2-2, w * frac, 4, C_ACC)
|
||||
end
|
||||
slider.Slider.Knob.Paint = function(s, w, h)
|
||||
draw.RoundedBox(w/2, 0, 0, w, h, C_PRI)
|
||||
if s:IsHovered() then draw.RoundedBox(w/2, 2, 2, w-4, h-4, C_ACC_H) end
|
||||
end
|
||||
slider.OnValueChanged = function(s, value) currentBodygroups[idx] = math.floor(value) end
|
||||
|
||||
yPos = yPos + 70
|
||||
end
|
||||
end
|
||||
|
||||
if #availableSkins > 1 then
|
||||
local skinLabel = vgui.Create("DPanel", settingsScroll)
|
||||
skinLabel:SetPos(10, yPos)
|
||||
skinLabel:SetSize(400, 35)
|
||||
skinLabel.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, 0, w, h, C_BG_L)
|
||||
surface.SetDrawColor(C_ACC)
|
||||
surface.DrawRect(0, 0, 3, h)
|
||||
draw.SimpleText("СКИНЫ", "DermaLarge", 10, h/2, C_TXT_P, TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
yPos = yPos + 40
|
||||
|
||||
local skinPanel = vgui.Create("DPanel", settingsScroll)
|
||||
skinPanel:SetPos(10, yPos)
|
||||
skinPanel:SetSize(400, 65)
|
||||
skinPanel.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_BG_L or C_BG_D
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 1)
|
||||
end
|
||||
|
||||
local skinSlider = vgui.Create("DNumSlider", skinPanel)
|
||||
skinSlider:SetPos(5, 15)
|
||||
skinSlider:SetSize(390, 40)
|
||||
skinSlider:SetMin(0)
|
||||
skinSlider:SetMax(#availableSkins)
|
||||
skinSlider:SetDecimals(0)
|
||||
skinSlider:SetValue(currentSkin)
|
||||
skinSlider:SetText("Скин")
|
||||
skinSlider.Slider.Paint = function(s, w, h)
|
||||
draw.RoundedBox(4, 0, h/2-2, w, 4, C_BG_D)
|
||||
local maxV = math.max(1, skinSlider:GetMax() - skinSlider:GetMin())
|
||||
local frac = (skinSlider:GetValue() - skinSlider:GetMin()) / maxV
|
||||
draw.RoundedBox(4, 0, h/2-2, w * frac, 4, C_ACC)
|
||||
end
|
||||
skinSlider.Slider.Knob.Paint = function(s, w, h)
|
||||
draw.RoundedBox(w/2, 0, 0, w, h, C_PRI)
|
||||
if s:IsHovered() then draw.RoundedBox(w/2, 2, 2, w-4, h-4, C_ACC_H) end
|
||||
end
|
||||
skinSlider.OnValueChanged = function(s, value) currentSkin = math.floor(value) end
|
||||
|
||||
yPos = yPos + 70
|
||||
end
|
||||
|
||||
local wipeBtn = vgui.Create("DButton", settingsPanel)
|
||||
wipeBtn:SetPos(10, 390)
|
||||
wipeBtn:SetSize(420, 50)
|
||||
wipeBtn:SetText("")
|
||||
wipeBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_WARN_H or C_WARN
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
draw.SimpleText("СБРОСИТЬ", "DermaLarge", w/2, h/2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
wipeBtn.DoClick = function()
|
||||
net.Start("ixWardrobeApply")
|
||||
net.WriteBool(true)
|
||||
net.SendToServer()
|
||||
frame:Close()
|
||||
end
|
||||
|
||||
local applyBtn = vgui.Create("DButton", settingsPanel)
|
||||
applyBtn:SetPos(10, 450)
|
||||
applyBtn:SetSize(420, 60)
|
||||
applyBtn:SetText("")
|
||||
applyBtn.Paint = function(s, w, h)
|
||||
local col = s:IsHovered() and C_PRI_H or C_PRI
|
||||
draw.RoundedBox(6, 0, 0, w, h, col)
|
||||
surface.SetDrawColor(C_GRAD)
|
||||
surface.DrawRect(0, 0, w, h/2)
|
||||
surface.SetDrawColor(C_BDR)
|
||||
surface.DrawOutlinedRect(0, 0, w, h, 2)
|
||||
draw.SimpleText("ПРИМЕНИТЬ", "DermaLarge", w/2, h/2, C_TXT_P, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
applyBtn.DoClick = function()
|
||||
net.Start("ixWardrobeApply")
|
||||
net.WriteBool(false)
|
||||
net.WriteTable(currentBodygroups)
|
||||
net.WriteUInt(currentSkin, 8)
|
||||
net.WriteUInt(currentPatch, 8)
|
||||
net.SendToServer()
|
||||
frame:Close()
|
||||
end
|
||||
end)
|
||||
end
|
||||
979
garrysmod/gamemodes/militaryrp/plugins/arsenal/sh_config.lua
Normal file
979
garrysmod/gamemodes/militaryrp/plugins/arsenal/sh_config.lua
Normal file
@@ -0,0 +1,979 @@
|
||||
-- проверка пениса
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PLUGIN.config = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
currencyName = "Очки снабжения",
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Начальное снабжение для фракций
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
startSupply = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[FACTION_RUSSIAN or 1] = 2000,
|
||||
[FACTION_UKRAINE or 2] = 2000,
|
||||
}, -- [FACTION_ID] = amount
|
||||
|
||||
-- Максиммальное снабжение
|
||||
maxSupply = 20000,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Минимум, при котором арсенал считается закрытым
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
minSupply = 0,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Cooldown for free weapons (supplyPrice == 0), per-player, seconds
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
freeWeaponCooldown = 3600, -- default 1 hour
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Currency symbols per faction (client uses this to display money symbol)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
currencySymbol = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[FACTION_RUSSIAN or 1] = "₽",
|
||||
[FACTION_UKRAINE or 2] = "$",
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
weapons = {
|
||||
["tfa_inss_wpn_ak12"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 11000,
|
||||
["supplyPrice"] = 220,
|
||||
["name"] = "AK-12"
|
||||
},
|
||||
["tacrp_mr96"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "MR-96"
|
||||
},
|
||||
["tacrp_ak_ak74u"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 3000,
|
||||
["supplyPrice"] = 50,
|
||||
["name"] = "AKС-74У"
|
||||
},
|
||||
["tacrp_io_glock18"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "Glock 18C"
|
||||
},
|
||||
["tacrp_ks23"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "КС-23"
|
||||
},
|
||||
["tacrp_as50"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 20000,
|
||||
["supplyPrice"] = 1000,
|
||||
["name"] = "AS50"
|
||||
},
|
||||
["weapon_lvsrepair"] = {
|
||||
["category"] = "tool1",
|
||||
["moneyPrice"] = 100,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Сварка"
|
||||
},
|
||||
["tacrp_io_trg42"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 450,
|
||||
["name"] = "Sako TRG-42"
|
||||
},
|
||||
["tacrp_sd_dual_degala"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "Двойные Deagle"
|
||||
},
|
||||
["weapon_cigarette_camel"] = {
|
||||
["category"] = "tool1",
|
||||
["moneyPrice"] = 100,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Сигаретка"
|
||||
},
|
||||
["tacrp_io_sg550"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "SIG SG 550"
|
||||
},
|
||||
["tacrp_io_rpk"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 150,
|
||||
["name"] = "РПК-74"
|
||||
},
|
||||
["tacrp_uzi"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "UZI"
|
||||
},
|
||||
["parachute_swep"] = {
|
||||
["category"] = "tool4",
|
||||
["moneyPrice"] = 150,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Парашют"
|
||||
},
|
||||
["tacrp_hk417"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "HK 417"
|
||||
},
|
||||
["tacrp_sd_bizon"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "ПП-19 Бизон"
|
||||
},
|
||||
["weapon_sw_at4"] = {
|
||||
["moneyPrice"] = 4500,
|
||||
["supplyPrice"] = 450,
|
||||
["maxCount"] = 1,
|
||||
["ammoType"] = "rpg_round",
|
||||
["category"] = "heavy",
|
||||
["maxAmmo"] = 1,
|
||||
["name"] = "AT4"
|
||||
},
|
||||
["fas2_ifak"] = {
|
||||
["category"] = "tool7",
|
||||
["moneyPrice"] = 500,
|
||||
["supplyPrice"] = 50,
|
||||
["name"] = "Аптечка"
|
||||
},
|
||||
["bandage"] = {
|
||||
["category"] = "tool7",
|
||||
["moneyPrice"] = 500,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "Бинт"
|
||||
},
|
||||
["tacrp_io_saiga"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "Сайга-12К"
|
||||
},
|
||||
["tacrp_nade_frag"] = {
|
||||
["category"] = "grenade",
|
||||
["moneyPrice"] = 300,
|
||||
["supplyPrice"] = 30,
|
||||
["name"] = "Осколочная граната"
|
||||
},
|
||||
["tacrp_g36k"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "G36K"
|
||||
},
|
||||
["tfa_inss_wpn_l1a1"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 240,
|
||||
["name"] = "L1A1"
|
||||
},
|
||||
["bodycam_tablet"] = {
|
||||
["category"] = "tool3",
|
||||
["moneyPrice"] = 500,
|
||||
["supplyPrice"] = 50,
|
||||
["name"] = "Боди-камера"
|
||||
},
|
||||
["gmod_camera"] = {
|
||||
["category"] = "tool3",
|
||||
["moneyPrice"] = 50,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Камера"
|
||||
},
|
||||
["admin_defib"] = {
|
||||
["category"] = "tool7",
|
||||
["moneyPrice"] = 8000,
|
||||
["supplyPrice"] = 5000,
|
||||
["name"] = "Дефибриллятор"
|
||||
},
|
||||
["tacrp_sg551"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 20000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "SG 551"
|
||||
},
|
||||
["tacrp_knife"] = {
|
||||
["category"] = "melee",
|
||||
["moneyPrice"] = 50,
|
||||
["supplyPrice"] = 0,
|
||||
["name"] = "Нож"
|
||||
},
|
||||
["guitar"] = {
|
||||
["category"] = "tool3",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Гитара"
|
||||
},
|
||||
["v92_bf2_medikit"] = {
|
||||
["category"] = "tool9",
|
||||
["moneyPrice"] = 1500,
|
||||
["supplyPrice"] = 150,
|
||||
["name"] = "Аптечка"
|
||||
},
|
||||
["weapon_sw_9k38"] = {
|
||||
["category"] = "heavy",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 500,
|
||||
["name"] = "9K38"
|
||||
},
|
||||
["tacrp_mp7"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "MP7"
|
||||
},
|
||||
["weapon_lvsspikestrip"] = {
|
||||
["category"] = "tool2",
|
||||
["moneyPrice"] = 100,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Шипы"
|
||||
},
|
||||
["tacrp_p250"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "P250"
|
||||
},
|
||||
["tacrp_m4"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "HK416"
|
||||
},
|
||||
["tfa_inss_wpn_m16a4"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 240,
|
||||
["name"] = "M16A4"
|
||||
},
|
||||
["tacrp_ak_svd"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 450,
|
||||
["name"] = "СВД"
|
||||
},
|
||||
["tacrp_io_xm8lmg"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "ХМ8 LMG"
|
||||
},
|
||||
["tacrp_mg4"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "HK MG4"
|
||||
},
|
||||
["tacrp_io_degala"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "Desert Eagle"
|
||||
},
|
||||
["tacrp_mp5"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "MP5A3"
|
||||
},
|
||||
["special_bandage"] = {
|
||||
["category"] = "tool8",
|
||||
["moneyPrice"] = 1000,
|
||||
["supplyPrice"] = 500,
|
||||
["name"] = "Шина от переломов"
|
||||
},
|
||||
["tacrp_ak_aek971"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "АЕК-971"
|
||||
},
|
||||
["tacrp_ak_ak12"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 11000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "АК-12"
|
||||
},
|
||||
["weapon_sw_rpg26"] = {
|
||||
["moneyPrice"] = 4500,
|
||||
["supplyPrice"] = 450,
|
||||
["maxCount"] = 1,
|
||||
["ammoType"] = "rpg_round",
|
||||
["category"] = "heavy",
|
||||
["maxAmmo"] = 1,
|
||||
["name"] = "РПГ-26"
|
||||
},
|
||||
["tacrp_spr"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "M700"
|
||||
},
|
||||
["tacrp_ex_m4a1"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "М4А1"
|
||||
},
|
||||
["tfa_new_inss_mk18"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 240,
|
||||
["name"] = "MK18"
|
||||
},
|
||||
["tacrp_io_fiveseven"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "FiveSeven"
|
||||
},
|
||||
["engineertoolmines"] = {
|
||||
["category"] = "tool5",
|
||||
["moneyPrice"] = 50000,
|
||||
["supplyPrice"] = 1000,
|
||||
["name"] = "Мины"
|
||||
},
|
||||
["tacrp_pdw"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 3000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "KAC PDW"
|
||||
},
|
||||
["tacrp_ak_an94"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "АН-94"
|
||||
},
|
||||
["weapon_r_handcuffs"] = {
|
||||
["category"] = "tool8",
|
||||
["moneyPrice"] = 1000,
|
||||
["supplyPrice"] = 10,
|
||||
["name"] = "Наручники"
|
||||
},
|
||||
["tfa_inss_wpn_fn_fal"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 240,
|
||||
["name"] = "FN FAL"
|
||||
},
|
||||
["weapon_rope_knife"] = {
|
||||
["category"] = "tool6",
|
||||
["moneyPrice"] = 50000,
|
||||
["supplyPrice"] = 500,
|
||||
["name"] = "Крюк кошка"
|
||||
},
|
||||
["tacrp_io_sg550r"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "SG-550R"
|
||||
},
|
||||
["weapon_sw_fim92"] = {
|
||||
["category"] = "heavy",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 500,
|
||||
["name"] = "FIM-92"
|
||||
},
|
||||
["tacrp_sd_groza"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "ОЦ-14"
|
||||
},
|
||||
["tacrp_ifak"] = {
|
||||
["category"] = "tool7",
|
||||
["moneyPrice"] = 500,
|
||||
["supplyPrice"] = 50,
|
||||
["name"] = "Аптечка IFAK"
|
||||
},
|
||||
["tacrp_aug"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 250,
|
||||
["name"] = "AUG"
|
||||
},
|
||||
["weapon_sw_panzerfaust3"] = {
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 500,
|
||||
["maxCount"] = 1,
|
||||
["ammoType"] = "PanzerFaust3 Rocket",
|
||||
["category"] = "heavy",
|
||||
["maxAmmo"] = 1,
|
||||
["name"] = "Панцерфауст 3"
|
||||
},
|
||||
["tacrp_io_scarh"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "FN SCAR"
|
||||
},
|
||||
["tacrp_io_xm8car"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 250,
|
||||
["name"] = "XM8"
|
||||
},
|
||||
["tacrp_pa_makarov"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "ПМ"
|
||||
},
|
||||
["tacrp_bekas"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "Bekas"
|
||||
},
|
||||
["weapon_cuff_elastic"] = {
|
||||
["category"] = "tool8",
|
||||
["moneyPrice"] = 300,
|
||||
["supplyPrice"] = 20,
|
||||
["name"] = "Наручники"
|
||||
},
|
||||
["tacrp_ex_hecate"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 17000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "PGM Hecate"
|
||||
},
|
||||
["tacrp_ex_glock"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 20,
|
||||
["name"] = "Glock"
|
||||
},
|
||||
["tfa_ins2_aks_r"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 11000,
|
||||
["supplyPrice"] = 220,
|
||||
["name"] = "AKS-74U"
|
||||
},
|
||||
["tacrp_ak_ak74"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 50,
|
||||
["name"] = "АК-74"
|
||||
},
|
||||
["tacrp_io_sl8"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 9000,
|
||||
["supplyPrice"] = 180,
|
||||
["name"] = "SL8"
|
||||
},
|
||||
["tfa_ins2_moe_akm"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 12000,
|
||||
["supplyPrice"] = 240,
|
||||
["name"] = "AKM MOE"
|
||||
},
|
||||
["tacrp_io_val"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "АС ВАЛ"
|
||||
},
|
||||
["tacrp_skorpion"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "VZ-61"
|
||||
},
|
||||
["swep_drone_grenade"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 1000,
|
||||
["name"] = "Дрон с гранатой"
|
||||
},
|
||||
["tacrp_sd_pkm"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "ПКМ"
|
||||
},
|
||||
["weapon_sw_rpg28"] = {
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 500,
|
||||
["maxCount"] = 1,
|
||||
["ammoType"] = "rpg_round",
|
||||
["category"] = "heavy",
|
||||
["maxAmmo"] = 1,
|
||||
["name"] = "РПГ-28"
|
||||
},
|
||||
["tacrp_nade_flashbang"] = {
|
||||
["category"] = "grenade1",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 20,
|
||||
["name"] = "Светошумовая граната"
|
||||
},
|
||||
["v92_bf2_ammokit"] = {
|
||||
["category"] = "tool3",
|
||||
["moneyPrice"] = 1500,
|
||||
["supplyPrice"] = 150,
|
||||
["name"] = "Ящик с патронами"
|
||||
},
|
||||
["tacrp_io_vss"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "ВСС Винторез"
|
||||
},
|
||||
["tacrp_io_p226"] = {
|
||||
["category"] = "secondary",
|
||||
["moneyPrice"] = 200,
|
||||
["supplyPrice"] = 1,
|
||||
["name"] = "P226"
|
||||
},
|
||||
["tfa_blast_ksvk_cqb"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 15000,
|
||||
["supplyPrice"] = 300,
|
||||
["name"] = "KSvK 12.7"
|
||||
},
|
||||
["tacrp_io_k98"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 5000,
|
||||
["supplyPrice"] = 100,
|
||||
["name"] = "KAR98K"
|
||||
},
|
||||
["tacrp_superv"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 9000,
|
||||
["supplyPrice"] = 180,
|
||||
["name"] = "KRISS Vector"
|
||||
},
|
||||
["tacrp_nade_smoke"] = {
|
||||
["category"] = "grenade2",
|
||||
["moneyPrice"] = 150,
|
||||
["supplyPrice"] = 15,
|
||||
["name"] = "Дымовая граната"
|
||||
},
|
||||
["tacrp_sd_aac_hb"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "AAC Honey badger"
|
||||
},
|
||||
["tacrp_pa_vykhlop"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 17000,
|
||||
["supplyPrice"] = 350,
|
||||
["name"] = "ВСК Выхлоп"
|
||||
},
|
||||
["tacrp_sd_g3"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 10000,
|
||||
["supplyPrice"] = 200,
|
||||
["name"] = "G3A3"
|
||||
},
|
||||
["tfa_ins2_warface_cheytac_m200"] = {
|
||||
["category"] = "primary",
|
||||
["moneyPrice"] = 17000,
|
||||
["supplyPrice"] = 340,
|
||||
["name"] = "Cheytac M200"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- ============================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- DONATE WEAPONS (donate_only = true означает что доступно ТОЛЬКО через донат)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Если donate_only НЕ указано или false, оружие может быть добавлено в spec/podr и будет платным
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- ============================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Патроны: тип -> {name, price, amount}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ammo = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- ["rifle"] = { name = "Патроны для винтовки", price = 10, amount = 30 },
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- Броня (опционально)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
armor = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- amount: how much armor will be set on the player after purchase
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- supplyPrice: cost in faction supply points (keeps naming consistent with weapons)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
["armor_light"] = { name = "Легкая броня", supplyPrice = 50, moneyPrice = 0, amount = 25 },
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
["armor_medium"] = { name = "Средняя броня", supplyPrice = 75, moneyPrice = 0, amount = 50 },
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
["armor_heavy"] = { name = "Тяжелая броня", supplyPrice = 150, moneyPrice = 0, amount = 100 },
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
wardrobeBlacklist = {},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
patchIndex = 15,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
patchMaterials = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"models/texture_vsu_1amb/path",
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"models/texture_vsu_1amb/path2",
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"models/texture_vsu_1amb/path3"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
wardrobeAccess = {},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
wardrobeStats = {},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
wardrobeCHands = {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
64
garrysmod/gamemodes/militaryrp/plugins/arsenal/sh_plugin.lua
Normal file
64
garrysmod/gamemodes/militaryrp/plugins/arsenal/sh_plugin.lua
Normal file
@@ -0,0 +1,64 @@
|
||||
local PLUGIN = PLUGIN
|
||||
PLUGIN.name = "Arsenal"
|
||||
PLUGIN.author = "Refosel"
|
||||
PLUGIN.description = "Arsenal plugin (skeleton) — выдача оружия, патронов и управление очками снабжения."
|
||||
|
||||
ix.util.Include("sh_config.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
|
||||
|
||||
ix.command.Add("test_arsenal", {
|
||||
description = "Test arsenal weapon availability.",
|
||||
OnRun = function(self, client)
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then return "Plugin not found" end
|
||||
|
||||
local char = client:GetCharacter()
|
||||
if not char then return "No character" end
|
||||
|
||||
client:ChatPrint("--- Arsenal Test for " .. client:Nick() .. " ---")
|
||||
local avail = plugin:GetAvailableWeapons(char)
|
||||
client:ChatPrint("Available weapons total: " .. table.Count(avail))
|
||||
for class, data in pairs(avail) do
|
||||
client:ChatPrint(string.format(" [%s] %s (Donate: %s)", class, data.name or "N/A", tostring(data.isDonateVersion or false)))
|
||||
end
|
||||
client:ChatPrint("--- End Test ---")
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("arsenal_supply_set", {
|
||||
description = "Set faction supply. Usage: arsenal_supply_set <amount> [faction id]",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.number,
|
||||
ix.type.number
|
||||
},
|
||||
OnRun = function(self, client, amount, faction)
|
||||
if CLIENT then return end
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then return client:Notify("Arsenal plugin not loaded") end
|
||||
|
||||
local fid = faction or client:GetCharacter():GetFaction()
|
||||
plugin:SetFactionSupply(fid, math.max(tonumber(amount) or 0, 0))
|
||||
client:Notify("Set supply for faction " .. tostring(fid) .. " to " .. tostring(plugin:GetFactionSupply(fid)))
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("arsenal_supply_add", {
|
||||
description = "Add (or subtract) supply for a faction. Usage: arsenal_supply_add <delta> [faction id]",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.number,
|
||||
ix.type.number
|
||||
},
|
||||
OnRun = function(self, client, delta, faction)
|
||||
if CLIENT then return end
|
||||
local plugin = ix.plugin.list and ix.plugin.list["arsenal"]
|
||||
if not plugin then return client:Notify("Arsenal plugin not loaded") end
|
||||
|
||||
local fid = faction or client:GetCharacter():GetFaction()
|
||||
plugin:AddFactionSupply(fid, tonumber(delta) or 0)
|
||||
client:Notify("Faction " .. tostring(fid) .. " supply now: " .. tostring(plugin:GetFactionSupply(fid)))
|
||||
end
|
||||
})
|
||||
751
garrysmod/gamemodes/militaryrp/plugins/arsenal/sv_plugin.lua
Normal file
751
garrysmod/gamemodes/militaryrp/plugins/arsenal/sv_plugin.lua
Normal file
@@ -0,0 +1,751 @@
|
||||
if SERVER then
|
||||
util.AddNetworkString("ixArsenalOpen")
|
||||
util.AddNetworkString("ixArsenalAction")
|
||||
util.AddNetworkString("ixWardrobeOpen")
|
||||
util.AddNetworkString("ixWardrobeApply")
|
||||
end
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
local config = PLUGIN.config
|
||||
|
||||
|
||||
|
||||
local function IsRecruit(ply)
|
||||
local char = ply:GetCharacter()
|
||||
if not char then return false end
|
||||
local spec = char.GetSpec and char:GetSpec() or 0
|
||||
return spec == 1
|
||||
end
|
||||
|
||||
-- Helpers to store complex data as JSON strings because SetData/GetData may not accept tables
|
||||
local function _loadJSONData(plugin, key)
|
||||
-- plugin:GetData() returns the whole plugin store (or default table). We keep subkeys inside that store.
|
||||
local store = plugin:GetData() or {}
|
||||
if type(store) ~= "table" then return {} end
|
||||
local raw = store[key]
|
||||
if raw == nil then return {} end
|
||||
if type(raw) == "table" then return raw end
|
||||
if type(raw) == "string" then
|
||||
local ok, tbl = pcall(util.JSONToTable, raw)
|
||||
if ok and istable(tbl) then return tbl end
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function _saveJSONData(plugin, key, tbl)
|
||||
if type(tbl) ~= "table" then tbl = {} end
|
||||
local store = plugin:GetData() or {}
|
||||
if type(store) ~= "table" then store = {} end
|
||||
-- store the table directly under the subkey; ix.data.Set will serialize the whole store
|
||||
store[key] = tbl
|
||||
plugin:SetData(store)
|
||||
end
|
||||
|
||||
-- Faction supply storage helpers
|
||||
function PLUGIN:GetFactionSupply(faction)
|
||||
local store = _loadJSONData(self, "factionSupply") or {}
|
||||
local key = tostring(faction)
|
||||
local val = store[faction]
|
||||
if val == nil then val = store[key] end
|
||||
if val == nil then
|
||||
-- fallback to configured startSupply or 0
|
||||
return config.startSupply[faction] or 0
|
||||
end
|
||||
local maxSupply = (self.config and self.config.maxSupply) or 20000
|
||||
return math.Clamp(tonumber(val) or 0, 0, maxSupply)
|
||||
end
|
||||
|
||||
-- Returns remaining cooldown seconds for free weapons for a specific player (per-player persistent)
|
||||
function PLUGIN:GetFreeWeaponCooldownForPlayer(client)
|
||||
if not IsValid(client) or not client:IsPlayer() then return 0 end
|
||||
local stamps = _loadJSONData(self, "freeWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
local last = tonumber(stamps[steam]) or 0
|
||||
local now = os.time()
|
||||
local cd = tonumber(config.freeWeaponCooldown) or 0
|
||||
if cd <= 0 then return 0 end
|
||||
local remain = cd - (now - last)
|
||||
if remain < 0 then remain = 0 end
|
||||
return math.floor(remain)
|
||||
end
|
||||
|
||||
-- Returns remaining cooldown seconds for donate weapons (10 minutes = 600 seconds)
|
||||
function PLUGIN:GetDonateWeaponCooldown(client, weaponClass)
|
||||
if not IsValid(client) or not client:IsPlayer() then return 0 end
|
||||
local stamps = _loadJSONData(self, "donateWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
if not stamps[steam] then return 0 end
|
||||
local last = tonumber(stamps[steam][weaponClass]) or 0
|
||||
local now = os.time()
|
||||
local cd = 600 -- 10 минут
|
||||
local remain = cd - (now - last)
|
||||
if remain < 0 then remain = 0 end
|
||||
return math.floor(remain)
|
||||
end
|
||||
|
||||
function PLUGIN:SetDonateWeaponCooldown(client, weaponClass)
|
||||
if not IsValid(client) or not client:IsPlayer() then return end
|
||||
local stamps = _loadJSONData(self, "donateWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
if not stamps[steam] then stamps[steam] = {} end
|
||||
stamps[steam][weaponClass] = os.time()
|
||||
_saveJSONData(self, "donateWeaponTimestamps", stamps)
|
||||
end
|
||||
|
||||
function PLUGIN:SetFactionSupply(faction, amount)
|
||||
local store = _loadJSONData(self, "factionSupply") or {}
|
||||
local key = tostring(faction)
|
||||
local value = math.max(tonumber(amount) or 0, 0)
|
||||
store[key] = value
|
||||
store[faction] = value
|
||||
_saveJSONData(self, "factionSupply", store)
|
||||
end
|
||||
|
||||
-- Compatibility helper: add (or subtract) supply for a faction
|
||||
function PLUGIN:AddFactionSupply(faction, delta)
|
||||
local amount = tonumber(delta) or 0
|
||||
local cur = tonumber(self:GetFactionSupply(faction)) or 0
|
||||
local maxSupply = (self.config and self.config.maxSupply) or 20000
|
||||
local new = math.Clamp(cur + amount, 0, maxSupply)
|
||||
|
||||
if new == cur then return end -- Если ничего не изменилось (например, уперлись в лимит)
|
||||
|
||||
self:SetFactionSupply(faction, new)
|
||||
|
||||
-- Логирование изменения снабжения
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if (serverlogsPlugin) then
|
||||
local factionName = ix.faction.Get(faction).name or tostring(faction)
|
||||
if amount > 0 then
|
||||
local message = string.format("Фракция '%s' получила +%d очков снабжения (итого: %d)", factionName, amount, new)
|
||||
serverlogsPlugin:AddLog("FACTION_SUPPLY_ADD", message, nil, {
|
||||
faction = faction,
|
||||
factionName = factionName,
|
||||
amount = amount,
|
||||
total = new
|
||||
})
|
||||
elseif amount < 0 then
|
||||
local message = string.format("Фракция '%s' потратила %d очков снабжения (итого: %d)", factionName, math.abs(amount), new)
|
||||
serverlogsPlugin:AddLog("FACTION_SUPPLY_USE", message, nil, {
|
||||
faction = faction,
|
||||
factionName = factionName,
|
||||
amount = math.abs(amount),
|
||||
total = new
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
-- Получить список доступного оружия для персонажа (заглушка)
|
||||
-- Рекомендуется реализовать: читать FACTION.Spec и FACTION.Podr из ix.faction.Get(faction)
|
||||
function PLUGIN:GetAvailableWeapons(char)
|
||||
local weapons = {}
|
||||
local faction = char:GetFaction()
|
||||
local factionTable = ix.faction.Get(faction)
|
||||
if not factionTable then
|
||||
return weapons
|
||||
end
|
||||
|
||||
local allowed = {}
|
||||
local specIndex = tonumber(char:GetSpec()) or 1
|
||||
if factionTable.Spec and factionTable.Spec[specIndex] and istable(factionTable.Spec[specIndex].weapons) then
|
||||
for _, c in ipairs(factionTable.Spec[specIndex].weapons) do
|
||||
allowed[c] = true
|
||||
end
|
||||
end
|
||||
|
||||
local podrIndex = tonumber(char:GetPodr()) or 1
|
||||
if factionTable.Podr and factionTable.Podr[podrIndex] and istable(factionTable.Podr[podrIndex].preset) then
|
||||
for _, c in ipairs(factionTable.Podr[podrIndex].preset) do
|
||||
allowed[c] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- donate weapons from character data (with expiration check)
|
||||
local donateWeaponsTimed = char:GetData("donate_weapons_timed", {})
|
||||
local donateTable = {}
|
||||
if donateWeaponsTimed and istable(donateWeaponsTimed) then
|
||||
local now = os.time()
|
||||
for wepClass, wepData in pairs(donateWeaponsTimed) do
|
||||
if istable(wepData) and wepData.expires and tonumber(wepData.expires) > now then
|
||||
donateTable[wepClass] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Поддержка старой системы без срока (если есть)
|
||||
local donateList = char:GetData("donate_weapons", false)
|
||||
if donateList and istable(donateList) then
|
||||
for _, c in ipairs(donateList) do donateTable[c] = true end
|
||||
end
|
||||
|
||||
-- IGS integration: check if player has any weapons bought via IGS
|
||||
local client = char:GetPlayer()
|
||||
if IsValid(client) then
|
||||
if IGS then
|
||||
if isfunction(IGS.GetItems) then
|
||||
local items = IGS.GetItems()
|
||||
for _, ITEM in pairs(items) do
|
||||
local uid = (isfunction(ITEM.GetUID) and ITEM:GetUID()) or ITEM.uid
|
||||
if not uid then continue end
|
||||
|
||||
local has = false
|
||||
if isfunction(client.HasPurchase) then
|
||||
has = client:HasPurchase(uid)
|
||||
elseif isfunction(IGS.PlayerHasItem) then
|
||||
has = IGS.PlayerHasItem(client, uid)
|
||||
end
|
||||
|
||||
if has then
|
||||
-- Try all possible ways IGS stores the weapon class
|
||||
local weaponClass = nil
|
||||
local weaponCandidates = {
|
||||
(isfunction(ITEM.GetWeapon) and ITEM:GetWeapon()),
|
||||
(isfunction(ITEM.GetMeta) and ITEM:GetMeta("weapon")),
|
||||
ITEM.weapon,
|
||||
ITEM.weapon_class,
|
||||
ITEM.class,
|
||||
ITEM.uniqueID,
|
||||
ITEM.val,
|
||||
uid
|
||||
}
|
||||
for _, candidate in ipairs(weaponCandidates) do
|
||||
if candidate and candidate ~= "" then
|
||||
weaponClass = candidate
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if weaponClass and type(weaponClass) ~= "string" then
|
||||
weaponClass = tostring(weaponClass)
|
||||
end
|
||||
|
||||
if not weaponClass then
|
||||
weaponClass = uid
|
||||
end
|
||||
|
||||
if weaponClass then
|
||||
-- Check if this class or a similar one exists in config
|
||||
if config.weapons[weaponClass] then
|
||||
donateTable[weaponClass] = true
|
||||
else
|
||||
for configClass, _ in pairs(config.weapons) do
|
||||
if configClass:find(weaponClass, 1, true) then
|
||||
donateTable[configClass] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Build result from config only if class is allowed and exists in config
|
||||
local supply = tonumber(self:GetFactionSupply(faction)) or 0
|
||||
for class, _ in pairs(allowed) do
|
||||
local data = config.weapons[class]
|
||||
if not data then continue end -- skip if not configured
|
||||
|
||||
-- Проверяем есть ли оружие в донате у игрока
|
||||
local hasDonate = donateTable[class]
|
||||
|
||||
if hasDonate then
|
||||
-- Если куплено в донате - показываем как донатное (бесплатно, с кулдауном)
|
||||
local donateData = table.Copy(data)
|
||||
donateData.isDonateVersion = true -- флаг что это донатная версия
|
||||
donateData.supplyPrice = 0
|
||||
donateData.moneyPrice = 0
|
||||
weapons[class] = donateData
|
||||
elseif data.donate_only then
|
||||
-- Оружие только для доната, но не куплено - не показываем
|
||||
continue
|
||||
else
|
||||
-- Обычное оружие из spec/podr - показываем с ценой из конфига
|
||||
if supply <= 0 then
|
||||
if (data.supplyPrice or 0) == 0 then weapons[class] = data end
|
||||
else
|
||||
weapons[class] = data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Добавляем оружие которое есть ТОЛЬКО в донате (не в spec/podr)
|
||||
for wepClass, _ in pairs(donateTable) do
|
||||
if not allowed[wepClass] then
|
||||
local data = config.weapons[wepClass]
|
||||
if data then
|
||||
local donateData = table.Copy(data)
|
||||
donateData.isDonateVersion = true
|
||||
donateData.supplyPrice = 0
|
||||
donateData.moneyPrice = 0
|
||||
weapons[wepClass] = donateData
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return weapons
|
||||
end
|
||||
|
||||
-- Заглушки для действий (реализуйте логику по необходимости)
|
||||
function PLUGIN:BuyWeapon(client, weaponClass, payMethod)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return false, "Нет персонажа" end
|
||||
local available = self:GetAvailableWeapons(char)
|
||||
local data = available[weaponClass]
|
||||
if not data then return false, "Оружие недоступно для покупки" end
|
||||
payMethod = payMethod or "supply"
|
||||
local faction = char:GetFaction()
|
||||
|
||||
-- Option A: pay by money (Helix)
|
||||
if payMethod == "money" then
|
||||
local price = data.moneyPrice or 0
|
||||
if price == 0 then return false, "Это оружие нельзя купить за деньги" end
|
||||
if not char.HasMoney or not char:HasMoney(price) then return false, "Недостаточно денег" end
|
||||
char:TakeMoney(price)
|
||||
client:Give(weaponClass)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Check if this is a donate weapon version
|
||||
local isDonateWeapon = (data.isDonateVersion == true)
|
||||
|
||||
if isDonateWeapon then
|
||||
-- Проверяем что оружие уже есть у игрока
|
||||
if client:HasWeapon(weaponClass) then
|
||||
return false, "У вас уже есть это оружие"
|
||||
end
|
||||
|
||||
-- Проверяем кулдаун для донат-оружия (10 минут)
|
||||
local cooldown = self:GetDonateWeaponCooldown(client, weaponClass)
|
||||
if cooldown > 0 then
|
||||
local minutes = math.floor(cooldown / 60)
|
||||
local seconds = cooldown % 60
|
||||
return false, string.format("Вы недавно брали это оружие. Подождите %02d:%02d", minutes, seconds)
|
||||
end
|
||||
|
||||
-- Проверяем категорию (донат оружие тоже может иметь категорию)
|
||||
local newCat = data.category or ""
|
||||
if newCat ~= "" and not newCat:find("^tool") then
|
||||
for _, wep in ipairs(client:GetWeapons()) do
|
||||
if not IsValid(wep) then continue end
|
||||
local wclass = wep:GetClass()
|
||||
local wdata = config.weapons[wclass]
|
||||
if wdata and wdata.category == newCat then
|
||||
return false, "Вы уже имеете оружие этой категории: " .. newCat
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check max ammo limit for launchers
|
||||
if data.maxAmmo then
|
||||
local totalAmmo = 0
|
||||
if data.ammoType == "PanzerFaust3 Rocket" or data.ammoType == "rpg_round" then
|
||||
totalAmmo = client:GetAmmoCount("rpg_round") + client:GetAmmoCount("PanzerFaust3 Rocket")
|
||||
else
|
||||
totalAmmo = client:GetAmmoCount(data.ammoType or "rpg_round")
|
||||
end
|
||||
if totalAmmo >= data.maxAmmo then
|
||||
return false, "Вы достигли максимального количества снарядов для этого типа оружия (" .. tostring(data.maxAmmo) .. ")"
|
||||
end
|
||||
end
|
||||
|
||||
-- Выдаем оружие и ставим кулдаун
|
||||
client:Give(weaponClass)
|
||||
|
||||
-- Reset ammo for launchers to prevent extra ammo
|
||||
if data.ammoType then
|
||||
client:SetAmmo(0, data.ammoType)
|
||||
end
|
||||
|
||||
self:SetDonateWeaponCooldown(client, weaponClass)
|
||||
|
||||
-- Логирование выдачи донат-оружия
|
||||
end
|
||||
|
||||
-- Default: pay by supply
|
||||
local supplyCost = data.supplyPrice or 0
|
||||
local factionSupply = self:GetFactionSupply(faction)
|
||||
|
||||
-- If weapon is free (supplyCost == 0), enforce per-player persistent cooldown
|
||||
if supplyCost == 0 then
|
||||
local stamps = _loadJSONData(self, "freeWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
local last = tonumber(stamps[steam]) or 0
|
||||
local now = os.time()
|
||||
local cd = tonumber(config.freeWeaponCooldown) or 0
|
||||
if cd > 0 and (now - last) < cd then
|
||||
local remain = cd - (now - last)
|
||||
return false, "Вы уже брали бесплатное оружие недавно. Подождите " .. tostring(remain) .. " сек."
|
||||
end
|
||||
-- don't write timestamp yet: write after successful give
|
||||
else
|
||||
if supplyCost > 0 then
|
||||
if factionSupply < supplyCost then return false, "Недостаточно очков снабжения у фракции" end
|
||||
self:AddFactionSupply(faction, -supplyCost)
|
||||
end
|
||||
end
|
||||
-- Check category conflict: prevent two weapons from same category
|
||||
local newCat = data.category or ""
|
||||
if newCat ~= "" and not newCat:find("^tool") then
|
||||
for _, wep in ipairs(client:GetWeapons()) do
|
||||
if not IsValid(wep) then continue end
|
||||
local wclass = wep:GetClass()
|
||||
local wdata = config.weapons[wclass]
|
||||
if wdata and wdata.category == newCat then
|
||||
return false, "Вы уже имеете оружие этой категории: " .. newCat
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check max ammo limit for launchers
|
||||
if data.maxAmmo then
|
||||
local totalAmmo = client:GetAmmoCount(data.ammoType or "rpg_round")
|
||||
if totalAmmo >= data.maxAmmo then
|
||||
return false, "Вы достигли максимального количества снарядов для этого типа оружия (" .. tostring(data.maxAmmo) .. ")"
|
||||
end
|
||||
end
|
||||
-- Check max count limit for weapons
|
||||
if data.maxCount and client:HasWeapon(weaponClass) then
|
||||
return false, "У вас уже есть это оружие"
|
||||
end
|
||||
|
||||
client:Give(weaponClass)
|
||||
|
||||
-- Reset ammo for launchers to prevent extra ammo
|
||||
if data.ammoType then
|
||||
client:SetAmmo(0, data.ammoType)
|
||||
end
|
||||
-- If free weapon granted, save timestamp per-player
|
||||
if supplyCost == 0 then
|
||||
local stamps = _loadJSONData(self, "freeWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
stamps[steam] = os.time()
|
||||
_saveJSONData(self, "freeWeaponTimestamps", stamps)
|
||||
end
|
||||
|
||||
-- Логирование выдачи оружия из арсенала
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if (serverlogsPlugin) then
|
||||
local weaponName = data.name or weaponClass
|
||||
local factionName = ix.faction.Get(faction).name or tostring(faction)
|
||||
local message = string.format("%s получил оружие '%s' из арсенала (стоимость: %d снабжения)", client:Nick(), weaponName, supplyCost)
|
||||
serverlogsPlugin:AddLog("WEAPON_SPAWN", message, client, {
|
||||
weaponClass = weaponClass,
|
||||
weaponName = weaponName,
|
||||
supplyCost = supplyCost,
|
||||
faction = faction,
|
||||
factionName = factionName
|
||||
})
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function PLUGIN:BuyAmmo(client, ammoType)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return false, "Нет персонажа" end
|
||||
|
||||
-- Запрет на покупку патронов для ракетниц
|
||||
if ammoType == "rpg_round" or ammoType == "PanzerFaust3 Rocket" then
|
||||
return false, "Патроны для этого типа оружия недоступны для покупки"
|
||||
end
|
||||
|
||||
local ammoData = config.ammo[ammoType]
|
||||
if not ammoData then return false, "Тип патронов не найден" end
|
||||
|
||||
local faction = char:GetFaction()
|
||||
local supply = self:GetFactionSupply(faction)
|
||||
|
||||
local price = ammoData.price or 0
|
||||
if price > 0 then
|
||||
if supply < price then return false, "Недостаточно очков снабжения у фракции" end
|
||||
self:AddFactionSupply(faction, -price)
|
||||
end
|
||||
|
||||
client:GiveAmmo(ammoData.amount or 30, ammoType, true)
|
||||
return true
|
||||
end
|
||||
|
||||
function PLUGIN:ReturnWeapon(client, weaponClass)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return false, "Нет персонажа" end
|
||||
local data = config.weapons[weaponClass]
|
||||
if not data then return false, "Оружие не найдено" end
|
||||
|
||||
local faction = char:GetFaction()
|
||||
-- Remove weapon
|
||||
client:StripWeapon(weaponClass)
|
||||
|
||||
-- Reset ammo for launchers to prevent extra ammo
|
||||
if data.ammoType then
|
||||
client:SetAmmo(0, data.ammoType)
|
||||
end
|
||||
|
||||
-- Если это донат-оружие, сбрасываем кулдаун
|
||||
if data.donate == true then
|
||||
local stamps = _loadJSONData(self, "donateWeaponTimestamps") or {}
|
||||
local steam = client:SteamID() or tostring(client:SteamID64() or "unknown")
|
||||
if stamps[steam] and stamps[steam][weaponClass] then
|
||||
stamps[steam][weaponClass] = nil
|
||||
_saveJSONData(self, "donateWeaponTimestamps", stamps)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local refund = math.floor((data.supplyPrice or 0) * 0.8)
|
||||
if refund > 0 then self:AddFactionSupply(faction, refund) end
|
||||
return true
|
||||
end
|
||||
|
||||
function PLUGIN:SaveData()
|
||||
local data = self:GetData() or {}
|
||||
data.entities = {}
|
||||
|
||||
for _, entity in ipairs(ents.FindByClass("ix_arsenal")) do
|
||||
data.entities[#data.entities + 1] = {
|
||||
class = "ix_arsenal",
|
||||
pos = entity:GetPos(),
|
||||
angles = entity:GetAngles()
|
||||
}
|
||||
end
|
||||
|
||||
for _, entity in ipairs(ents.FindByClass("ix_ammobox")) do
|
||||
data.entities[#data.entities + 1] = {
|
||||
class = "ix_ammobox",
|
||||
pos = entity:GetPos(),
|
||||
angles = entity:GetAngles()
|
||||
}
|
||||
end
|
||||
|
||||
for _, entity in ipairs(ents.FindByClass("ix_wardrobe")) do
|
||||
data.entities[#data.entities + 1] = {
|
||||
class = "ix_wardrobe",
|
||||
pos = entity:GetPos(),
|
||||
angles = entity:GetAngles()
|
||||
}
|
||||
end
|
||||
|
||||
self:SetData(data)
|
||||
end
|
||||
|
||||
function PLUGIN:LoadData()
|
||||
local data = self:GetData() or {}
|
||||
local entities = data.entities or data -- fallback to compatibility if no 'entities' key
|
||||
|
||||
for _, v in ipairs(entities) do
|
||||
if not v.class then continue end
|
||||
local entity = ents.Create(v.class or "ix_arsenal")
|
||||
entity:SetPos(v.pos)
|
||||
entity:SetAngles(v.angles)
|
||||
entity:Spawn()
|
||||
|
||||
local phys = entity:GetPhysicsObject()
|
||||
if IsValid(phys) then
|
||||
phys:EnableMotion(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Обработчик сетевых действий от клиента
|
||||
if SERVER then
|
||||
net.Receive("ixArsenalAction", function(len, ply)
|
||||
local action = net.ReadString()
|
||||
local id = net.ReadString()
|
||||
local plugin = ix.plugin.list["arsenal"]
|
||||
if not plugin then return end
|
||||
|
||||
if IsRecruit(ply) then
|
||||
ply:Notify("Новоприбывший не может пользоваться арсеналом.")
|
||||
return
|
||||
end
|
||||
|
||||
if action == "buy_weapon" then
|
||||
local method = net.ReadString()
|
||||
local ok, msg = plugin:BuyWeapon(ply, id, method)
|
||||
if not ok then
|
||||
ply:Notify(msg or "Ошибка покупки оружия")
|
||||
end
|
||||
elseif action == "buy_ammo" then
|
||||
local ok, msg = plugin:BuyAmmo(ply, id)
|
||||
if not ok then
|
||||
ply:Notify(msg or "Ошибка покупки патронов")
|
||||
end
|
||||
elseif action == "return_weapon" then
|
||||
local ok, msg = plugin:ReturnWeapon(ply, id)
|
||||
if not ok then
|
||||
ply:Notify(msg or "Ошибка возврата оружия")
|
||||
end
|
||||
elseif action == "buy_armor" then
|
||||
local method = net.ReadString()
|
||||
local ok, msg = plugin:BuyArmor(ply, id, method)
|
||||
if not ok then
|
||||
ply:Notify(msg or "Ошибка покупки брони")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Buy armor implementation
|
||||
function PLUGIN:BuyArmor(client, armorClass, payMethod)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return false, "Нет персонажа" end
|
||||
local aData = config.armor[armorClass]
|
||||
if not aData then return false, "Броня не найдена" end
|
||||
payMethod = payMethod or "supply"
|
||||
|
||||
-- Check current armor: Armor() is the player's current armor value
|
||||
local curArmor = tonumber(client:Armor()) or 0
|
||||
local giveAmount = tonumber(aData.amount) or 0
|
||||
if curArmor >= giveAmount then
|
||||
return false, "Вы не можете купить броню — у вас уже есть броня этого уровня или выше"
|
||||
end
|
||||
|
||||
local faction = char:GetFaction()
|
||||
|
||||
-- money path
|
||||
if payMethod == "money" then
|
||||
local price = aData.moneyPrice or 0
|
||||
if price <= 0 then return false, "Эту броню нельзя купить за деньги" end
|
||||
if not char.HasMoney or not char:HasMoney(price) then return false, "Недостаточно денег" end
|
||||
char:TakeMoney(price)
|
||||
client:SetArmor(giveAmount)
|
||||
return true
|
||||
end
|
||||
|
||||
-- supply path
|
||||
local supplyCost = aData.supplyPrice or 0
|
||||
local factionSupply = self:GetFactionSupply(faction)
|
||||
if supplyCost > 0 then
|
||||
if factionSupply < supplyCost then return false, "Недостаточно очков снабжения у фракции" end
|
||||
self:AddFactionSupply(faction, -supplyCost)
|
||||
end
|
||||
|
||||
client:SetArmor(giveAmount)
|
||||
return true
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerLoadedCharacter(client, character, currentChar)
|
||||
if not IsValid(client) then return end
|
||||
timer.Simple(0.5, function()
|
||||
if not IsValid(client) or not client:GetCharacter() then return end
|
||||
local char = client:GetCharacter()
|
||||
local bodygroups = char:GetData("bodygroups", {})
|
||||
local skin = char:GetData("skin", 0)
|
||||
local patch = char:GetData("patchIndex", 1)
|
||||
for idx, value in pairs(bodygroups) do
|
||||
client:SetBodygroup(idx, value)
|
||||
end
|
||||
client:SetSkin(skin)
|
||||
if self.config.patchIndex and self.config.patchMaterials then
|
||||
local pMat = self.config.patchMaterials[patch]
|
||||
if pMat then
|
||||
client:SetSubMaterial(self.config.patchIndex, pMat)
|
||||
else
|
||||
client:SetSubMaterial(self.config.patchIndex, "")
|
||||
end
|
||||
end
|
||||
hook.Run("PostPlayerLoadout", client)
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:PostPlayerLoadout(client)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
local model = client:GetModel()
|
||||
local bgs = char:GetData("bodygroups", {})
|
||||
local speedMod = 1
|
||||
local maxArmor = 100
|
||||
local ammoMod = 1
|
||||
local stats = self.config.wardrobeStats and self.config.wardrobeStats[model]
|
||||
if stats then
|
||||
for idx, val in pairs(bgs) do
|
||||
if stats[idx] and stats[idx][val] then
|
||||
local st = stats[idx][val]
|
||||
if st.speed then speedMod = speedMod * st.speed end
|
||||
if st.armor then maxArmor = maxArmor + st.armor end
|
||||
if st.ammo then ammoMod = ammoMod * st.ammo end
|
||||
end
|
||||
end
|
||||
end
|
||||
client:SetWalkSpeed(ix.config.Get("walkSpeed") * speedMod)
|
||||
client:SetRunSpeed(ix.config.Get("runSpeed") * speedMod)
|
||||
client:SetMaxArmor(maxArmor)
|
||||
char:SetData("ammoMod", ammoMod)
|
||||
end
|
||||
|
||||
function PLUGIN:EntityTakeDamage(target, dmginfo)
|
||||
if not target:IsPlayer() then return end
|
||||
local char = target:GetCharacter()
|
||||
if not char then return end
|
||||
local model = target:GetModel()
|
||||
local bgs = char:GetData("bodygroups", {})
|
||||
local resist = 1
|
||||
local stats = self.config.wardrobeStats and self.config.wardrobeStats[model]
|
||||
if stats then
|
||||
for idx, val in pairs(bgs) do
|
||||
if stats[idx] and stats[idx][val] then
|
||||
local st = stats[idx][val]
|
||||
if st.dmgResist then
|
||||
resist = resist * st.dmgResist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if resist ~= 1 then
|
||||
dmginfo:ScaleDamage(resist)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:InitPostEntity()
|
||||
-- Уменьшаем задержку до 2 секунд для более быстрой очистки при старте
|
||||
timer.Simple(2, function()
|
||||
-- Загружаем сырые данные из стора напрямую
|
||||
local currentData = _loadJSONData(self, "factionSupply") or {}
|
||||
|
||||
-- Перебираем все записи в сторе. Это надежнее, чем идти по индексам фракций,
|
||||
-- так как это затронет любые старые или "кривые" ключи в базе.
|
||||
local changed = false
|
||||
for key, value in pairs(currentData) do
|
||||
local factionID = tonumber(key)
|
||||
local rawVal = tonumber(value) or 0
|
||||
|
||||
if factionID and rawVal > 5000 then
|
||||
local startVal = (self.config.startSupply and self.config.startSupply[factionID]) or 2000
|
||||
|
||||
-- Устанавливаем новое значение
|
||||
currentData[key] = startVal
|
||||
currentData[factionID] = startVal
|
||||
changed = true
|
||||
|
||||
print(string.format("[Arsenal] Автосброс при старте: фракция %s, было %d, стало %d", key, rawVal, startVal))
|
||||
end
|
||||
end
|
||||
|
||||
if changed then
|
||||
_saveJSONData(self, "factionSupply", currentData)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerSetHandsModel(client, ent)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
local model = client:GetModel()
|
||||
local bgs = char:GetData("bodygroups", {})
|
||||
local chands = self.config.wardrobeCHands and self.config.wardrobeCHands[model]
|
||||
if chands then
|
||||
for idx, val in pairs(bgs) do
|
||||
if chands[idx] and chands[idx][val] then
|
||||
local hData = chands[idx][val]
|
||||
ent:SetModel(hData.model or "models/weapons/c_arms_cstrike.mdl")
|
||||
ent:SetSkin(hData.skin or 0)
|
||||
ent:SetBodyGroups(hData.bodygroups or "0000000")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
function AdvDupe2.ReceiveFile(data, autoSave)
|
||||
AdvDupe2.RemoveProgressBar()
|
||||
if not data then
|
||||
AdvDupe2.Notify("File was not saved! (No data)",NOTIFY_ERROR,5)
|
||||
return
|
||||
end
|
||||
local path
|
||||
if autoSave then
|
||||
if(LocalPlayer():GetInfo("advdupe2_auto_save_overwrite")~="0")then
|
||||
path = AdvDupe2.GetFilename(AdvDupe2.AutoSavePath, true)
|
||||
else
|
||||
path = AdvDupe2.GetFilename(AdvDupe2.AutoSavePath)
|
||||
end
|
||||
else
|
||||
path = AdvDupe2.GetFilename(AdvDupe2.SavePath)
|
||||
end
|
||||
|
||||
path = AdvDupe2.SanitizeFilename(path)
|
||||
local dupefile = file.Open(path, "wb", "DATA")
|
||||
if not dupefile then
|
||||
AdvDupe2.Notify("File was not saved! (Could not open file for writing)",NOTIFY_ERROR,5)
|
||||
return
|
||||
end
|
||||
dupefile:Write(data)
|
||||
dupefile:Close()
|
||||
|
||||
local errored = false
|
||||
if(LocalPlayer():GetInfo("advdupe2_debug_openfile")=="1")then
|
||||
if(not file.Exists(path, "DATA"))then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end
|
||||
|
||||
local readFile = file.Open(path, "rb", "DATA")
|
||||
if not readFile then AdvDupe2.Notify("File could not be read", NOTIFY_ERROR) return end
|
||||
local readData = readFile:Read(readFile:Size())
|
||||
readFile:Close()
|
||||
local success, dupe, _info, _moreinfo = AdvDupe2.Decode(readData)
|
||||
if(success)then
|
||||
AdvDupe2.Notify("DEBUG CHECK: File successfully opens. No EOF errors.")
|
||||
else
|
||||
AdvDupe2.Notify("DEBUG CHECK: " .. dupe, NOTIFY_ERROR)
|
||||
errored = true
|
||||
end
|
||||
end
|
||||
|
||||
local filename = string.StripExtension(string.GetFileFromFilename( path ))
|
||||
if autoSave then
|
||||
if(IsValid(AdvDupe2.FileBrowser.AutoSaveNode))then
|
||||
local add = true
|
||||
for i=1, #AdvDupe2.FileBrowser.AutoSaveNode.Files do
|
||||
if(filename==AdvDupe2.FileBrowser.AutoSaveNode.Files[i].Label:GetText())then
|
||||
add=false
|
||||
break
|
||||
end
|
||||
end
|
||||
if(add)then
|
||||
AdvDupe2.FileBrowser.AutoSaveNode:AddFile(filename)
|
||||
AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.AutoSaveNode)
|
||||
end
|
||||
end
|
||||
else
|
||||
AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode:AddFile(filename)
|
||||
AdvDupe2.FileBrowser.Browser.pnlCanvas:Sort(AdvDupe2.FileBrowser.Browser.pnlCanvas.ActionNode)
|
||||
end
|
||||
if not errored then
|
||||
AdvDupe2.Notify("File successfully saved!",NOTIFY_GENERIC, 5)
|
||||
end
|
||||
end
|
||||
|
||||
net.Receive("AdvDupe2_ReceiveFile", function()
|
||||
local autoSave = net.ReadBool()
|
||||
net.ReadStream(nil, function(data)
|
||||
AdvDupe2.ReceiveFile(data, autoSave)
|
||||
end)
|
||||
end)
|
||||
|
||||
AdvDupe2.Uploading = false
|
||||
function AdvDupe2.SendFile(name, data)
|
||||
net.Start("AdvDupe2_ReceiveFile")
|
||||
net.WriteString(name)
|
||||
AdvDupe2.Uploading = net.WriteStream(data, function()
|
||||
AdvDupe2.Uploading = nil
|
||||
AdvDupe2.File = nil
|
||||
AdvDupe2.RemoveProgressBar()
|
||||
end)
|
||||
net.SendToServer()
|
||||
end
|
||||
|
||||
function AdvDupe2.UploadFile(ReadPath, ReadArea)
|
||||
if AdvDupe2.Uploading then AdvDupe2.Notify("Already opening file, please wait.", NOTIFY_ERROR) return end
|
||||
if(ReadArea==0)then
|
||||
ReadPath = AdvDupe2.DataFolder.."/"..ReadPath..".txt"
|
||||
elseif(ReadArea==1)then
|
||||
ReadPath = AdvDupe2.DataFolder.."/-Public-/"..ReadPath..".txt"
|
||||
else
|
||||
ReadPath = "adv_duplicator/"..ReadPath..".txt"
|
||||
end
|
||||
|
||||
if(not file.Exists(ReadPath, "DATA"))then AdvDupe2.Notify("File does not exist", NOTIFY_ERROR) return end
|
||||
|
||||
local read = file.Read(ReadPath)
|
||||
if not read then AdvDupe2.Notify("File could not be read", NOTIFY_ERROR) return end
|
||||
local name = string.Explode("/", ReadPath)
|
||||
name = name[#name]
|
||||
name = string.sub(name, 1, #name-4)
|
||||
|
||||
local success, dupe, info, moreinfo = AdvDupe2.Decode(read)
|
||||
if(success)then
|
||||
AdvDupe2.SendFile(name, read)
|
||||
|
||||
AdvDupe2.LoadGhosts(dupe, info, moreinfo, name)
|
||||
else
|
||||
AdvDupe2.Notify("File could not be decoded. ("..dupe..") Upload Canceled.", NOTIFY_ERROR)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,353 @@
|
||||
function AdvDupe2.LoadGhosts(dupe, info, moreinfo, name, preview)
|
||||
AdvDupe2.RemoveGhosts()
|
||||
AdvDupe2.Ghosting = true
|
||||
AdvDupe2.GhostToSpawn = {}
|
||||
local count = 0
|
||||
local time, desc, date, creator
|
||||
|
||||
if(info.ad1) then
|
||||
local z = dupe.HeadEnt.Z
|
||||
local Pos, Ang
|
||||
|
||||
time = moreinfo.Time or ""
|
||||
desc = info.Description or ""
|
||||
date = info.Date or ""
|
||||
creator = info.Creator or ""
|
||||
|
||||
AdvDupe2.HeadEnt = dupe.HeadEnt.Index
|
||||
AdvDupe2.HeadPos = dupe.HeadEnt.Pos
|
||||
AdvDupe2.HeadZPos = z
|
||||
AdvDupe2.HeadPos.Z = AdvDupe2.HeadPos.Z + z
|
||||
|
||||
for k, v in pairs(dupe.Entities) do
|
||||
if(v.SavedParentIdx) then
|
||||
if(not v.BuildDupeInfo) then v.BuildDupeInfo = {} end
|
||||
v.BuildDupeInfo.DupeParentID = v.SavedParentIdx
|
||||
Pos = v.LocalPos
|
||||
Ang = v.LocalAngle
|
||||
else
|
||||
Pos, Ang = nil, nil
|
||||
end
|
||||
|
||||
for i, p in pairs(v.PhysicsObjects) do
|
||||
p.Pos = Pos or p.LocalPos
|
||||
p.Pos.Z = p.Pos.Z - z
|
||||
p.Angle = Ang or p.LocalAngle
|
||||
p.LocalPos = nil
|
||||
p.LocalAngle = nil
|
||||
end
|
||||
|
||||
v.LocalPos = nil
|
||||
v.LocalAngle = nil
|
||||
AdvDupe2.GhostToSpawn[count] =
|
||||
{
|
||||
Model = v.Model,
|
||||
PhysicsObjects = v.PhysicsObjects
|
||||
}
|
||||
|
||||
if(AdvDupe2.HeadEnt == k) then
|
||||
AdvDupe2.HeadEnt = count
|
||||
end
|
||||
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
AdvDupe2.HeadOffset = AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt].PhysicsObjects[0].Pos
|
||||
AdvDupe2.HeadAngle = AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt].PhysicsObjects[0].Angle
|
||||
else
|
||||
time = info.time or ""
|
||||
desc = dupe.Description or ""
|
||||
date = info.date or ""
|
||||
creator = info.name or ""
|
||||
|
||||
AdvDupe2.HeadEnt = dupe.HeadEnt.Index
|
||||
AdvDupe2.HeadZPos = dupe.HeadEnt.Z
|
||||
AdvDupe2.HeadPos = dupe.HeadEnt.Pos
|
||||
AdvDupe2.HeadOffset = dupe.Entities[AdvDupe2.HeadEnt].PhysicsObjects[0].Pos
|
||||
AdvDupe2.HeadAngle = dupe.Entities[AdvDupe2.HeadEnt].PhysicsObjects[0].Angle
|
||||
|
||||
for k, v in pairs(dupe.Entities) do
|
||||
AdvDupe2.GhostToSpawn[count] =
|
||||
{
|
||||
Model = v.Model,
|
||||
PhysicsObjects = v.PhysicsObjects
|
||||
}
|
||||
|
||||
if(AdvDupe2.HeadEnt == k) then
|
||||
AdvDupe2.HeadEnt = count
|
||||
end
|
||||
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
if(not preview) then
|
||||
AdvDupe2.Info.File:SetText("File: "..name)
|
||||
AdvDupe2.Info.Creator:SetText("Creator: "..creator)
|
||||
AdvDupe2.Info.Date:SetText("Date: "..date)
|
||||
AdvDupe2.Info.Time:SetText("Time: "..time)
|
||||
AdvDupe2.Info.Size:SetText("Size: "..string.NiceSize(tonumber(info.size) or 0))
|
||||
AdvDupe2.Info.Desc:SetText("Desc: "..(desc or ""))
|
||||
AdvDupe2.Info.Entities:SetText("Entities: "..table.Count(dupe.Entities))
|
||||
AdvDupe2.Info.Constraints:SetText("Constraints: "..table.Count(dupe.Constraints))
|
||||
end
|
||||
|
||||
AdvDupe2.StartGhosting()
|
||||
AdvDupe2.Preview = preview
|
||||
end
|
||||
|
||||
function AdvDupe2.RemoveGhosts()
|
||||
if(AdvDupe2.Ghosting) then
|
||||
hook.Remove("Tick", "AdvDupe2_SpawnGhosts")
|
||||
AdvDupe2.Ghosting = false
|
||||
|
||||
if(not AdvDupe2.BusyBar) then
|
||||
AdvDupe2.RemoveProgressBar()
|
||||
end
|
||||
end
|
||||
|
||||
if(AdvDupe2.GhostEntities) then
|
||||
for k, v in pairs(AdvDupe2.GhostEntities) do
|
||||
if(IsValid(v))then
|
||||
v:Remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if(IsValid(AdvDupe2.HeadGhost))then
|
||||
AdvDupe2.HeadGhost:Remove()
|
||||
end
|
||||
|
||||
AdvDupe2.CurrentGhost = 1
|
||||
AdvDupe2.HeadGhost = nil
|
||||
AdvDupe2.GhostEntities = nil
|
||||
AdvDupe2.Preview = false
|
||||
end
|
||||
|
||||
--Creates a ghost from the given entity's table
|
||||
local function MakeGhostsFromTable(EntTable)
|
||||
|
||||
if(not EntTable) then return end
|
||||
if(not EntTable.Model or EntTable.Model:sub(-4,-1) ~= ".mdl") then
|
||||
EntTable.Model = "models/error.mdl"
|
||||
end
|
||||
|
||||
local GhostEntity = ClientsideModel(EntTable.Model, RENDERGROUP_TRANSLUCENT)
|
||||
|
||||
-- If there are too many entities we might not spawn..
|
||||
if not IsValid(GhostEntity) then
|
||||
AdvDupe2.RemoveGhosts()
|
||||
AdvDupe2.Notify("Too many entities to spawn ghosts!", NOTIFY_ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
GhostEntity:SetRenderMode( RENDERMODE_TRANSALPHA ) --Was broken, making ghosts invisible
|
||||
GhostEntity:SetColor( Color(255, 255, 255, 150) )
|
||||
GhostEntity.Phys = EntTable.PhysicsObjects[0]
|
||||
|
||||
if util.IsValidRagdoll(EntTable.Model) then
|
||||
local ref, parents, angs = {}, {}, {}
|
||||
|
||||
GhostEntity:SetupBones()
|
||||
for k, v in pairs(EntTable.PhysicsObjects) do
|
||||
local bone = GhostEntity:TranslatePhysBoneToBone(k)
|
||||
local bonp = GhostEntity:GetBoneParent(bone)
|
||||
if bonp == -1 then
|
||||
ref[bone] = GhostEntity:GetBoneMatrix(bone):GetInverseTR()
|
||||
else
|
||||
bonp = GhostEntity:TranslatePhysBoneToBone(GhostEntity:TranslateBoneToPhysBone(bonp))
|
||||
parents[bone] = bonp
|
||||
ref[bone] = GhostEntity:GetBoneMatrix(bone):GetInverseTR() * GhostEntity:GetBoneMatrix(bonp)
|
||||
end
|
||||
|
||||
local m = Matrix() m:SetAngles(v.Angle)
|
||||
angs[bone] = m
|
||||
end
|
||||
|
||||
for bone, ang in pairs( angs ) do
|
||||
if parents[bone] and angs[parents[bone]] then
|
||||
local localrotation = angs[parents[bone]]:GetInverseTR() * ang
|
||||
local m = ref[bone] * localrotation
|
||||
GhostEntity:ManipulateBoneAngles(bone, m:GetAngles())
|
||||
else
|
||||
local pos = GhostEntity:GetBonePosition(bone)
|
||||
GhostEntity:ManipulateBonePosition(bone, -pos)
|
||||
GhostEntity:ManipulateBoneAngles(bone, ref[bone]:GetAngles())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return GhostEntity
|
||||
end
|
||||
|
||||
local function StopGhosting()
|
||||
AdvDupe2.Ghosting = false
|
||||
hook.Remove( "Tick", "AdvDupe2_SpawnGhosts" )
|
||||
|
||||
if not BusyBar then AdvDupe2.RemoveProgressBar() end
|
||||
end
|
||||
|
||||
local function SpawnGhosts()
|
||||
local ghostsPerTick = GetConVar( "advdupe2_ghost_rate" ):GetInt()
|
||||
local ghostPercentLimit = GetConVar( "advdupe2_limit_ghost" ):GetFloat()
|
||||
|
||||
local finalGhost = math.min( AdvDupe2.TotalGhosts, math.max( math.Round( (ghostPercentLimit / 100) * AdvDupe2.TotalGhosts ), 0 ) )
|
||||
local finalGhostInFrame = math.min( AdvDupe2.CurrentGhost + ghostsPerTick - 1, finalGhost )
|
||||
|
||||
for i = AdvDupe2.CurrentGhost, finalGhostInFrame do
|
||||
local g = AdvDupe2.GhostToSpawn[i]
|
||||
if g and i ~= AdvDupe2.HeadEnt then AdvDupe2.GhostEntities[i] = MakeGhostsFromTable( g ) end
|
||||
end
|
||||
AdvDupe2.CurrentGhost = finalGhostInFrame + 1
|
||||
|
||||
AdvDupe2.UpdateGhosts( true )
|
||||
if not AdvDupe2.BusyBar then
|
||||
AdvDupe2.ProgressBar.Percent = (AdvDupe2.CurrentGhost / AdvDupe2.TotalGhosts) * 100
|
||||
end
|
||||
|
||||
if AdvDupe2.CurrentGhost > finalGhost then
|
||||
StopGhosting()
|
||||
end
|
||||
end
|
||||
|
||||
net.Receive("AdvDupe2_SendGhosts", function(len, ply, len2)
|
||||
AdvDupe2.RemoveGhosts()
|
||||
AdvDupe2.GhostToSpawn = {}
|
||||
AdvDupe2.HeadEnt = net.ReadInt(16)
|
||||
AdvDupe2.HeadZPos = net.ReadFloat()
|
||||
AdvDupe2.HeadPos = net.ReadVector()
|
||||
|
||||
local cache = {}
|
||||
for i = 1, net.ReadInt(16) do
|
||||
cache[i] = net.ReadString()
|
||||
end
|
||||
|
||||
for i = 1, net.ReadInt(16) do
|
||||
AdvDupe2.GhostToSpawn[i] =
|
||||
{
|
||||
Model = cache[net.ReadInt(16)],
|
||||
PhysicsObjects = {}
|
||||
}
|
||||
|
||||
for k = 0, net.ReadInt(8) do
|
||||
AdvDupe2.GhostToSpawn[i].PhysicsObjects[k] =
|
||||
{
|
||||
Angle = net.ReadAngle(),
|
||||
Pos = net.ReadVector()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
AdvDupe2.CurrentGhost = 1
|
||||
AdvDupe2.GhostEntities = {}
|
||||
AdvDupe2.HeadGhost = MakeGhostsFromTable(AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt])
|
||||
AdvDupe2.HeadOffset = AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt].PhysicsObjects[0].Pos
|
||||
AdvDupe2.HeadAngle = AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt].PhysicsObjects[0].Angle
|
||||
AdvDupe2.GhostEntities[AdvDupe2.HeadEnt] = AdvDupe2.HeadGhost
|
||||
AdvDupe2.TotalGhosts = #AdvDupe2.GhostToSpawn
|
||||
|
||||
if(AdvDupe2.TotalGhosts > 1) then
|
||||
AdvDupe2.Ghosting = true
|
||||
|
||||
if(not AdvDupe2.BusyBar) then
|
||||
AdvDupe2.InitProgressBar("Ghosting: ")
|
||||
AdvDupe2.BusyBar = false
|
||||
end
|
||||
|
||||
hook.Add("Tick", "AdvDupe2_SpawnGhosts", SpawnGhosts)
|
||||
else
|
||||
AdvDupe2.Ghosting = false
|
||||
end
|
||||
end)
|
||||
|
||||
net.Receive("AdvDupe2_AddGhost", function(len, ply, len2)
|
||||
local ghost = {Model = net.ReadString(), PhysicsObjects = {}}
|
||||
for k = 0, net.ReadInt(8) do
|
||||
ghost.PhysicsObjects[k] = {Angle = net.ReadAngle(), Pos = net.ReadVector()}
|
||||
end
|
||||
|
||||
AdvDupe2.GhostEntities[AdvDupe2.CurrentGhost] = MakeGhostsFromTable(ghost)
|
||||
AdvDupe2.CurrentGhost = AdvDupe2.CurrentGhost + 1
|
||||
end)
|
||||
|
||||
function AdvDupe2.StartGhosting()
|
||||
AdvDupe2.RemoveGhosts()
|
||||
if(not AdvDupe2.GhostToSpawn) then return end
|
||||
AdvDupe2.CurrentGhost = 1
|
||||
AdvDupe2.GhostEntities = {}
|
||||
AdvDupe2.Ghosting = true
|
||||
AdvDupe2.HeadGhost = MakeGhostsFromTable(AdvDupe2.GhostToSpawn[AdvDupe2.HeadEnt])
|
||||
AdvDupe2.GhostEntities[AdvDupe2.HeadEnt] = AdvDupe2.HeadGhost
|
||||
AdvDupe2.TotalGhosts = #AdvDupe2.GhostToSpawn
|
||||
|
||||
if AdvDupe2.TotalGhosts > 1 then
|
||||
if not AdvDupe2.BusyBar then
|
||||
AdvDupe2.InitProgressBar("Ghosting: ")
|
||||
AdvDupe2.BusyBar = false
|
||||
end
|
||||
hook.Add("Tick", "AdvDupe2_SpawnGhosts", SpawnGhosts)
|
||||
else
|
||||
AdvDupe2.Ghosting = false
|
||||
end
|
||||
end
|
||||
net.Receive("AdvDupe2_StartGhosting", function()
|
||||
AdvDupe2.StartGhosting()
|
||||
end)
|
||||
|
||||
net.Receive("AdvDupe2_RemoveGhosts", AdvDupe2.RemoveGhosts)
|
||||
|
||||
--Update the ghost's postion and angles based on where the player is looking and the offsets
|
||||
local Lheadpos, Lheadang = Vector(), Angle()
|
||||
function AdvDupe2.UpdateGhosts(force)
|
||||
if not IsValid(AdvDupe2.HeadGhost) then
|
||||
AdvDupe2.RemoveGhosts()
|
||||
AdvDupe2.Notify("Invalid ghost parent!", NOTIFY_ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local trace = LocalPlayer():GetEyeTrace()
|
||||
if (not trace.Hit) then return end
|
||||
|
||||
local originpos, originang, headpos, headang
|
||||
local worigin = GetConVar("advdupe2_offset_world"):GetBool()
|
||||
if(GetConVar("advdupe2_original_origin"):GetBool())then
|
||||
originang = Angle()
|
||||
originpos = Vector(AdvDupe2.HeadPos)
|
||||
headpos = AdvDupe2.HeadPos + AdvDupe2.HeadOffset
|
||||
headang = AdvDupe2.HeadAngle
|
||||
else
|
||||
local hangle = worigin and Angle(0,0,0) or AdvDupe2.HeadAngle
|
||||
local pz = math.Clamp(AdvDupe2.HeadZPos + GetConVar("advdupe2_offset_z"):GetFloat() or 0, -16000, 16000)
|
||||
local ap = math.Clamp(GetConVar("advdupe2_offset_pitch"):GetFloat() or 0, -180, 180)
|
||||
local ay = math.Clamp(GetConVar("advdupe2_offset_yaw" ):GetFloat() or 0, -180, 180)
|
||||
local ar = math.Clamp(GetConVar("advdupe2_offset_roll" ):GetFloat() or 0, -180, 180)
|
||||
originang = Angle(ap, ay, ar)
|
||||
originpos = Vector(trace.HitPos); originpos.z = originpos.z + pz
|
||||
headpos, headang = LocalToWorld(AdvDupe2.HeadOffset, hangle, originpos, originang)
|
||||
end
|
||||
|
||||
if math.abs(Lheadpos.x - headpos.x) > 0.01 or
|
||||
math.abs(Lheadpos.y - headpos.y) > 0.01 or
|
||||
math.abs(Lheadpos.z - headpos.z) > 0.01 or
|
||||
math.abs(Lheadang.p - headang.p) > 0.01 or
|
||||
math.abs(Lheadang.y - headang.y) > 0.01 or
|
||||
math.abs(Lheadang.r - headang.r) > 0.01 or force then
|
||||
|
||||
Lheadpos = headpos
|
||||
Lheadang = headang
|
||||
|
||||
AdvDupe2.HeadGhost:SetPos(headpos)
|
||||
AdvDupe2.HeadGhost:SetAngles(headang)
|
||||
|
||||
for k, ghost in ipairs(AdvDupe2.GhostEntities) do
|
||||
local phys = ghost.Phys
|
||||
|
||||
if phys then
|
||||
local pos, ang = LocalToWorld(phys.Pos, phys.Angle, originpos, originang)
|
||||
ghost:SetPos(pos)
|
||||
ghost:SetAngles(ang)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,564 @@
|
||||
--[[
|
||||
Title: Adv. Dupe 2 Codec
|
||||
|
||||
Desc: Dupe encoder/decoder.
|
||||
|
||||
Author: emspike
|
||||
|
||||
Version: 2.0
|
||||
]]
|
||||
|
||||
local REVISION = 5
|
||||
AdvDupe2.CodecRevision = REVISION
|
||||
AdvDupe2.MaxDupeSize = 32e6 -- 32 MB
|
||||
|
||||
include( "sh_codec_legacy.lua" )
|
||||
AddCSLuaFile( "sh_codec_legacy.lua" )
|
||||
|
||||
local pairs = pairs
|
||||
local error = error
|
||||
local Vector = Vector
|
||||
local Angle = Angle
|
||||
local format = string.format
|
||||
local char = string.char
|
||||
local concat = table.concat
|
||||
local compress = util.Compress
|
||||
local decompress = util.Decompress
|
||||
|
||||
--[[
|
||||
Name: GenerateDupeStamp
|
||||
Desc: Generates an info table.
|
||||
Params: <player> ply
|
||||
Return: <table> stamp
|
||||
]]
|
||||
function AdvDupe2.GenerateDupeStamp(ply)
|
||||
local stamp = {}
|
||||
stamp.name = ply:GetName()
|
||||
stamp.time = os.date("%I:%M %p")
|
||||
stamp.date = os.date("%d %B %Y")
|
||||
stamp.timezone = os.date("%z")
|
||||
hook.Call("AdvDupe2_StampGenerated",GAMEMODE,stamp)
|
||||
return stamp
|
||||
end
|
||||
|
||||
function AdvDupe2.SanitizeFilename(filename)
|
||||
filename = string.gsub( filename, "[\":]", "_" )
|
||||
filename = string.gsub( filename, "%s+", " " )
|
||||
filename = string.gsub( filename, "%s*([\\/%.])%s*", "%1" )
|
||||
return filename
|
||||
end
|
||||
|
||||
local function makeInfo(tbl)
|
||||
local info = ""
|
||||
for k, v in pairs(tbl) do
|
||||
info = concat{info,k,"\1",v,"\1"}
|
||||
end
|
||||
return info.."\2"
|
||||
end
|
||||
|
||||
local AD2FF = "AD2F%s\n%s\n%s"
|
||||
|
||||
local tables, buff
|
||||
|
||||
local function noserializer() end
|
||||
|
||||
local enc = {}
|
||||
for i = 1, 255 do enc[i] = noserializer end
|
||||
|
||||
local function isArray(tbl)
|
||||
local ret = true
|
||||
local m = 0
|
||||
|
||||
for k, v in pairs(tbl) do
|
||||
m = m + 1
|
||||
if k ~= m or enc[TypeID(v)] == noserializer then
|
||||
ret = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function write(obj)
|
||||
enc[TypeID(obj)](obj)
|
||||
end
|
||||
|
||||
local len, tables, tablesLookup
|
||||
|
||||
enc[TYPE_TABLE] = function(obj) --table
|
||||
if not tablesLookup[obj] then
|
||||
tables = tables + 1
|
||||
tablesLookup[obj] = tables
|
||||
else
|
||||
buff:WriteByte(247)
|
||||
buff:WriteShort(tablesLookup[obj])
|
||||
return
|
||||
end
|
||||
|
||||
if isArray(obj) then
|
||||
buff:WriteByte(254)
|
||||
for i, v in pairs(obj) do
|
||||
write(v)
|
||||
end
|
||||
else
|
||||
buff:WriteByte(255)
|
||||
for k, v in pairs(obj) do
|
||||
if(enc[TypeID(k)] ~= noserializer and enc[TypeID(v)] ~= noserializer) then
|
||||
write(k)
|
||||
write(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
buff:WriteByte(246)
|
||||
end
|
||||
|
||||
enc[TYPE_BOOL] = function(obj) --boolean
|
||||
buff:WriteByte(obj and 253 or 252)
|
||||
end
|
||||
|
||||
enc[TYPE_NUMBER] = function(obj) --number
|
||||
buff:WriteByte(251)
|
||||
buff:WriteDouble(obj)
|
||||
end
|
||||
|
||||
enc[TYPE_VECTOR] = function(obj) --vector
|
||||
buff:WriteByte(250)
|
||||
buff:WriteDouble(obj.x)
|
||||
buff:WriteDouble(obj.y)
|
||||
buff:WriteDouble(obj.z)
|
||||
end
|
||||
|
||||
enc[TYPE_ANGLE] = function(obj) --angle
|
||||
buff:WriteByte(249)
|
||||
buff:WriteDouble(obj.p)
|
||||
buff:WriteDouble(obj.y)
|
||||
buff:WriteDouble(obj.r)
|
||||
end
|
||||
|
||||
enc[TYPE_STRING] = function(obj) --string
|
||||
len = #obj
|
||||
if len < 246 then
|
||||
buff:WriteByte(len)
|
||||
buff:Write(obj)
|
||||
else
|
||||
buff:WriteByte(248)
|
||||
buff:WriteULong(len)
|
||||
buff:Write(obj)
|
||||
end
|
||||
end
|
||||
|
||||
local function error_nodeserializer()
|
||||
buff:Seek(buff:Tell()-1)
|
||||
error(format("Couldn't find deserializer for type {typeid:%d}!", buff:ReadByte()))
|
||||
end
|
||||
|
||||
local reference = 0
|
||||
local read4, read5
|
||||
|
||||
do --Version 4
|
||||
local dec = {}
|
||||
for i = 1, 255 do dec[i] = error_nodeserializer end
|
||||
|
||||
local function read()
|
||||
local tt = buff:ReadByte()
|
||||
if not tt then
|
||||
error("Expected value, got EOF!")
|
||||
end
|
||||
if tt == 0 then
|
||||
return nil
|
||||
end
|
||||
return dec[tt]()
|
||||
end
|
||||
read4 = read
|
||||
|
||||
dec[255] = function() --table
|
||||
local t = {}
|
||||
local k
|
||||
reference = reference + 1
|
||||
local ref = reference
|
||||
repeat
|
||||
k = read()
|
||||
if k ~= nil then
|
||||
t[k] = read()
|
||||
end
|
||||
until (k == nil)
|
||||
tables[ref] = t
|
||||
return t
|
||||
end
|
||||
|
||||
dec[254] = function() --array
|
||||
local t = {}
|
||||
local k = 0
|
||||
local v
|
||||
reference = reference + 1
|
||||
local ref = reference
|
||||
repeat
|
||||
k = k + 1
|
||||
v = read()
|
||||
if(v ~= nil) then
|
||||
t[k] = v
|
||||
end
|
||||
|
||||
until (v == nil)
|
||||
tables[ref] = t
|
||||
return t
|
||||
end
|
||||
|
||||
dec[253] = function()
|
||||
return true
|
||||
end
|
||||
dec[252] = function()
|
||||
return false
|
||||
end
|
||||
dec[251] = function()
|
||||
return buff:ReadDouble()
|
||||
end
|
||||
dec[250] = function()
|
||||
return Vector(buff:ReadDouble(),buff:ReadDouble(),buff:ReadDouble())
|
||||
end
|
||||
dec[249] = function()
|
||||
return Angle(buff:ReadDouble(),buff:ReadDouble(),buff:ReadDouble())
|
||||
end
|
||||
dec[248] = function() --null-terminated string
|
||||
local start = buff:Tell()
|
||||
local slen = 0
|
||||
|
||||
while buff:ReadByte() ~= 0 do
|
||||
slen = slen + 1
|
||||
end
|
||||
|
||||
buff:Seek(start)
|
||||
|
||||
local retv = buff:Read(slen)
|
||||
if(not retv) then retv="" end
|
||||
buff:ReadByte()
|
||||
|
||||
return retv
|
||||
end
|
||||
dec[247] = function() --table reference
|
||||
reference = reference + 1
|
||||
return tables[buff:ReadShort()]
|
||||
end
|
||||
|
||||
for i = 1, 246 do dec[i] = function() return buff:Read(i) end end
|
||||
end
|
||||
|
||||
do --Version 5
|
||||
local dec = {}
|
||||
for i = 1, 255 do dec[i] = error_nodeserializer end
|
||||
|
||||
local function read()
|
||||
local tt = buff:ReadByte()
|
||||
if not tt then
|
||||
error("Expected value, got EOF!")
|
||||
end
|
||||
return dec[tt]()
|
||||
end
|
||||
read5 = read
|
||||
|
||||
dec[255] = function() --table
|
||||
local t = {}
|
||||
reference = reference + 1
|
||||
tables[reference] = t
|
||||
|
||||
for k in read do
|
||||
t[k] = read()
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
dec[254] = function() --array
|
||||
local t = {}
|
||||
reference = reference + 1
|
||||
tables[reference] = t
|
||||
|
||||
local k = 1
|
||||
for v in read do
|
||||
t[k] = v
|
||||
k = k + 1
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
dec[253] = function()
|
||||
return true
|
||||
end
|
||||
dec[252] = function()
|
||||
return false
|
||||
end
|
||||
dec[251] = function()
|
||||
return buff:ReadDouble()
|
||||
end
|
||||
dec[250] = function()
|
||||
return Vector(buff:ReadDouble(),buff:ReadDouble(),buff:ReadDouble())
|
||||
end
|
||||
dec[249] = function()
|
||||
return Angle(buff:ReadDouble(),buff:ReadDouble(),buff:ReadDouble())
|
||||
end
|
||||
dec[248] = function() -- Length>246 string
|
||||
local slen = buff:ReadULong()
|
||||
local retv = buff:Read(slen)
|
||||
if(not retv) then retv = "" end
|
||||
return retv
|
||||
end
|
||||
dec[247] = function() --table reference
|
||||
return tables[buff:ReadShort()]
|
||||
end
|
||||
dec[246] = function() --nil
|
||||
return
|
||||
end
|
||||
|
||||
for i = 1, 245 do dec[i] = function() return buff:Read(i) end end
|
||||
|
||||
dec[0] = function() return "" end
|
||||
end
|
||||
|
||||
local function serialize(tbl)
|
||||
tables = 0
|
||||
tablesLookup = {}
|
||||
|
||||
buff = file.Open("ad2temp.txt", "wb", "DATA")
|
||||
if not buff then error("Failed to open file data/ad2temp.txt for writing!") end
|
||||
write(tbl)
|
||||
buff:Close()
|
||||
|
||||
buff = file.Open("ad2temp.txt","rb","DATA")
|
||||
if not buff then error("Failed to open file data/ad2temp.txt for reading!") end
|
||||
local ret = buff:Read(buff:Size())
|
||||
buff:Close()
|
||||
return ret
|
||||
end
|
||||
|
||||
|
||||
local function deserialize(str, read)
|
||||
|
||||
if(str == nil) then
|
||||
error("File could not be decompressed!")
|
||||
return {}
|
||||
end
|
||||
|
||||
tables = {}
|
||||
reference = 0
|
||||
buff = file.Open("ad2temp.txt","wb","DATA")
|
||||
if not buff then error("Failed to open file data/ad2temp.txt for writing!") end
|
||||
buff:Write(str)
|
||||
buff:Flush()
|
||||
buff:Close()
|
||||
|
||||
buff = file.Open("ad2temp.txt","rb", "DATA")
|
||||
if not buff then error("Failed to open file data/ad2temp.txt for reading!") end
|
||||
local success, tbl = pcall(read)
|
||||
buff:Close()
|
||||
|
||||
if success then
|
||||
return tbl
|
||||
else
|
||||
error(tbl)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Name: Encode
|
||||
Desc: Generates the string for a dupe file with the given data.
|
||||
Params: <table> dupe, <table> info, <function> callback, <...> args
|
||||
Return: runs callback(<string> encoded_dupe, <...> args)
|
||||
]]
|
||||
function AdvDupe2.Encode(dupe, info, callback, ...)
|
||||
local encodedTable = compress(serialize(dupe))
|
||||
info.check = "\r\n\t\n"
|
||||
info.size = #encodedTable
|
||||
|
||||
callback(AD2FF:format(char(REVISION), makeInfo(info), encodedTable),...)
|
||||
end
|
||||
|
||||
--seperates the header and body and converts the header to a table
|
||||
local function getInfo(str)
|
||||
local last = str:find("\2")
|
||||
if not last then
|
||||
error("Attempt to read AD2 file with malformed info block!")
|
||||
end
|
||||
local info = {}
|
||||
local ss = str:sub(1, last - 1)
|
||||
for k, v in ss:gmatch("(.-)\1(.-)\1") do
|
||||
info[k] = v
|
||||
end
|
||||
|
||||
if info.check ~= "\r\n\t\n" then
|
||||
if info.check == "\10\9\10" then
|
||||
error("Detected AD2 file corrupted in file transfer (newlines homogenized)(when using FTP, transfer AD2 files in image/binary mode, not ASCII/text mode)!")
|
||||
elseif info.check ~= nil then
|
||||
error("Detected AD2 file corrupted by newline replacements (copy/pasting the data in various editors can cause this!)")
|
||||
else
|
||||
error("Attempt to read AD2 file with malformed info block!")
|
||||
end
|
||||
end
|
||||
return info, str:sub(last+2)
|
||||
end
|
||||
|
||||
--decoders for individual versions go here
|
||||
local versions = {}
|
||||
|
||||
versions[1] = AdvDupe2.LegacyDecoders[1]
|
||||
versions[2] = AdvDupe2.LegacyDecoders[2]
|
||||
|
||||
versions[3] = function(encodedDupe)
|
||||
encodedDupe = encodedDupe:Replace("\r\r\n\t\r\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n\t\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n", "\n")
|
||||
encodedDupe = encodedDupe:Replace("\t\t\t\t", "\r\n\t\n")
|
||||
return versions[4](encodedDupe)
|
||||
end
|
||||
|
||||
versions[4] = function(encodedDupe)
|
||||
local info, dupestring = getInfo(encodedDupe:sub(7))
|
||||
return deserialize(decompress(dupestring, AdvDupe2.MaxDupeSize), read4), info
|
||||
end
|
||||
|
||||
versions[5] = function(encodedDupe)
|
||||
local info, dupestring = getInfo(encodedDupe:sub(7))
|
||||
return deserialize(decompress(dupestring, AdvDupe2.MaxDupeSize), read5), info
|
||||
end
|
||||
|
||||
function AdvDupe2.CheckValidDupe(dupe, info)
|
||||
if not dupe.HeadEnt then return false, "Missing HeadEnt table" end
|
||||
if not dupe.Entities then return false, "Missing Entities table" end
|
||||
if not dupe.Constraints then return false, "Missing Constraints table" end
|
||||
if not dupe.HeadEnt.Z then return false, "Missing HeadEnt.Z" end
|
||||
if not dupe.HeadEnt.Pos then return false, "Missing HeadEnt.Pos" end
|
||||
if not dupe.HeadEnt.Index then return false, "Missing HeadEnt.Index" end
|
||||
if not dupe.Entities[dupe.HeadEnt.Index] then return false, "Missing HeadEnt index ["..dupe.HeadEnt.Index.."] from Entities table" end
|
||||
for key, data in pairs(dupe.Entities) do
|
||||
if not data.PhysicsObjects then return false, "Missing PhysicsObject table from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
if not data.PhysicsObjects[0] then return false, "Missing PhysicsObject[0] table from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
if info.ad1 then -- Advanced Duplicator 1
|
||||
if not data.PhysicsObjects[0].LocalPos then return false, "Missing PhysicsObject[0].LocalPos from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
if not data.PhysicsObjects[0].LocalAngle then return false, "Missing PhysicsObject[0].LocalAngle from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
else -- Advanced Duplicator 2
|
||||
if not data.PhysicsObjects[0].Pos then return false, "Missing PhysicsObject[0].Pos from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
if not data.PhysicsObjects[0].Angle then return false, "Missing PhysicsObject[0].Angle from Entity ["..key.."]["..data.Class.."]["..data.Model.."]" end
|
||||
end
|
||||
end
|
||||
return true, dupe
|
||||
end
|
||||
|
||||
--[[
|
||||
Name: Decode
|
||||
Desc: Generates the table for a dupe from the given string. Inverse of Encode
|
||||
Params: <string> encodedDupe, <function> callback, <...> args
|
||||
Return: runs callback(<boolean> success, <table/string> tbl, <table> info)
|
||||
]]
|
||||
function AdvDupe2.Decode(encodedDupe)
|
||||
|
||||
local sig, rev = encodedDupe:match("^(....)(.)")
|
||||
|
||||
if not rev then
|
||||
return false, "Malformed dupe (wtf <5 chars long)!"
|
||||
end
|
||||
|
||||
rev = rev:byte()
|
||||
|
||||
if sig ~= "AD2F" then
|
||||
if sig == "[Inf" then --legacy support, ENGAGE (AD1 dupe detected)
|
||||
local success, tbl, info, moreinfo = pcall(AdvDupe2.LegacyDecoders[0], encodedDupe)
|
||||
|
||||
if success then
|
||||
info.ad1 = true
|
||||
info.size = #encodedDupe
|
||||
info.revision = 0
|
||||
|
||||
local index = tonumber(moreinfo.Head) or (istable(tbl.Entities) and next(tbl.Entities))
|
||||
if not index then return false, "Missing head index" end
|
||||
local pos
|
||||
if isstring(moreinfo.StartPos) then
|
||||
local spx,spy,spz = moreinfo.StartPos:match("^(.-),(.-),(.+)$")
|
||||
pos = Vector(tonumber(spx) or 0, tonumber(spy) or 0, tonumber(spz) or 0)
|
||||
else
|
||||
pos = Vector()
|
||||
end
|
||||
local z
|
||||
if isstring(moreinfo.HoldPos) then
|
||||
z = (tonumber(moreinfo.HoldPos:match("^.-,.-,(.+)$")) or 0)*-1
|
||||
else
|
||||
z = 0
|
||||
end
|
||||
tbl.HeadEnt = {
|
||||
Index = index,
|
||||
Pos = pos,
|
||||
Z = z
|
||||
}
|
||||
else
|
||||
ErrorNoHalt(tbl)
|
||||
end
|
||||
|
||||
if success then
|
||||
success, tbl = AdvDupe2.CheckValidDupe(tbl, info)
|
||||
end
|
||||
|
||||
return success, tbl, info, moreinfo
|
||||
else
|
||||
return false, "Unknown duplication format!"
|
||||
end
|
||||
elseif rev > REVISION then
|
||||
return false, format("Newer codec needed. (have rev %u, need rev %u) Update Advdupe2.",REVISION,rev)
|
||||
elseif rev < 1 then
|
||||
return false, format("Attempt to use an invalid format revision (rev %d)!", rev)
|
||||
else
|
||||
local success, tbl, info = pcall(versions[rev], encodedDupe)
|
||||
|
||||
if success then
|
||||
success, tbl = AdvDupe2.CheckValidDupe(tbl, info)
|
||||
end
|
||||
if success then
|
||||
info.revision = rev
|
||||
end
|
||||
|
||||
return success, tbl, info
|
||||
end
|
||||
end
|
||||
|
||||
if CLIENT then
|
||||
|
||||
concommand.Add("advdupe2_to_json", function(_,_,arg)
|
||||
if not arg[1] then print("Need AdvDupe2 file name argument!") return end
|
||||
local readFileName = "advdupe2/"..arg[1]
|
||||
local writeFileName = "advdupe2/"..string.StripExtension(arg[1])..".json"
|
||||
|
||||
writeFileName = AdvDupe2.SanitizeFilename(writeFileName)
|
||||
|
||||
local readFile = file.Open(readFileName, "rb", "DATA")
|
||||
if not readFile then print("File could not be read or found! ("..readFileName..")") return end
|
||||
local readData = readFile:Read(readFile:Size())
|
||||
readFile:Close()
|
||||
local ok, tbl = AdvDupe2.Decode(readData)
|
||||
local writeFile = file.Open(writeFileName, "wb", "DATA")
|
||||
if not writeFile then print("File could not be written! ("..writeFileName..")") return end
|
||||
writeFile:Write(util.TableToJSON(tbl))
|
||||
writeFile:Close()
|
||||
print("File written! ("..writeFileName..")")
|
||||
end)
|
||||
|
||||
concommand.Add("advdupe2_from_json", function(_,_,arg)
|
||||
if not arg[1] then print("Need json file name argument!") return end
|
||||
local readFileName = "advdupe2/"..arg[1]
|
||||
local writeFileName = "advdupe2/"..string.StripExtension(arg[1])..".txt"
|
||||
|
||||
local readFile = file.Open(readFileName, "rb", "DATA")
|
||||
if not readFile then print("File could not be read or found! ("..readFileName..")") return end
|
||||
local readData = readFile:Read(readFile:Size())
|
||||
readFile:Close()
|
||||
|
||||
AdvDupe2.Encode(util.JSONToTable(readData), {}, function(data)
|
||||
local writeFile = file.Open(writeFileName, "wb", "DATA")
|
||||
if not writeFile then print("File could not be written! ("..writeFileName..")") return end
|
||||
writeFile:Write(data)
|
||||
writeFile:Close()
|
||||
print("File written! ("..writeFileName..")")
|
||||
end)
|
||||
end)
|
||||
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
--[[
|
||||
Title: Adv. Dupe 2 Codec Legacy Support
|
||||
|
||||
Desc: Facilitates opening of dupes from AD1 and earlier AD2 versions.
|
||||
|
||||
Author: emspike
|
||||
|
||||
Version: 2.0
|
||||
]]
|
||||
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local tonumber = tonumber
|
||||
local error = error
|
||||
local Vector = Vector
|
||||
local Angle = Angle
|
||||
local format = string.format
|
||||
local char = string.char
|
||||
local byte = string.byte
|
||||
local sub = string.sub
|
||||
local gsub = string.gsub
|
||||
local find = string.find
|
||||
local gmatch = string.gmatch
|
||||
local match = string.match
|
||||
local concat = table.concat
|
||||
|
||||
--[[
|
||||
Name: GenerateDupeStamp
|
||||
Desc: Generates an info table.
|
||||
Params: <player> ply
|
||||
Return: <table> stamp
|
||||
]]
|
||||
function AdvDupe2.GenerateDupeStamp(ply)
|
||||
local stamp = {}
|
||||
stamp.name = ply:GetName()
|
||||
stamp.time = os.date("%I:%M %p")
|
||||
stamp.date = os.date("%d %B %Y")
|
||||
stamp.timezone = os.date("%z")
|
||||
hook.Call("AdvDupe2_StampGenerated",GAMEMODE,stamp)
|
||||
return stamp
|
||||
end
|
||||
|
||||
local AD2FF = "AD2F%s\n%s\n%s"
|
||||
|
||||
local decode_types_v1, decode_types_v2
|
||||
local tables = 0
|
||||
local str,pos
|
||||
local a,b,c,m,n,w,tblref
|
||||
|
||||
local function read_v2()
|
||||
local t = byte(str, pos+1)
|
||||
if t then
|
||||
local dt = decode_types_v2[t]
|
||||
if dt then
|
||||
pos = pos + 1
|
||||
return dt()
|
||||
else
|
||||
error(format("encountered invalid data type (%u)\n",t))
|
||||
end
|
||||
else
|
||||
error("expected value, got EOF\n")
|
||||
end
|
||||
end
|
||||
|
||||
decode_types_v2 = {
|
||||
[1 ] = function()
|
||||
error("expected value, got terminator\n")
|
||||
end,
|
||||
[2 ] = function() -- table
|
||||
|
||||
m = find(str, "\1", pos)
|
||||
if m then
|
||||
w = sub(str, pos+1, m-1)
|
||||
pos = m
|
||||
else
|
||||
error("expected table identifier, got EOF\n")
|
||||
end
|
||||
|
||||
local t = {}
|
||||
tables[w] = t
|
||||
|
||||
while true do
|
||||
if byte(str, pos+1) == 1 then
|
||||
pos = pos + 1
|
||||
return t
|
||||
else
|
||||
t[read_v2()] = read_v2()
|
||||
end
|
||||
end
|
||||
end,
|
||||
[3 ] = function() -- array
|
||||
|
||||
m = find(str, "\1", pos)
|
||||
if m then
|
||||
w = sub(str, pos+1, m-1)
|
||||
pos = m
|
||||
else
|
||||
error("expected table identifier, got EOF\n")
|
||||
end
|
||||
|
||||
local t, i = {}, 1
|
||||
|
||||
tables[w] = t
|
||||
|
||||
while true do
|
||||
if byte(str,pos+1) == 1 then
|
||||
pos = pos+1
|
||||
return t
|
||||
else
|
||||
t[i] = read_v2()
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end,
|
||||
[4 ] = function() -- false boolean
|
||||
return false
|
||||
end,
|
||||
[5 ] = function() -- true boolean
|
||||
return true
|
||||
end,
|
||||
[6 ] = function() -- number
|
||||
m = find(str, "\1", pos)
|
||||
if m then
|
||||
a = tonumber(sub(str, pos+1, m-1)) or 0
|
||||
pos = m
|
||||
return a
|
||||
else
|
||||
error("expected number, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[7 ] = function() -- string
|
||||
m = find(str,"\1",pos)
|
||||
if m then
|
||||
w = sub(str, pos+1, m-1)
|
||||
pos = m
|
||||
return w
|
||||
else
|
||||
error("expected string, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[8 ] = function() -- Vector
|
||||
m,n = find(str,".-\1.-\1.-\1", pos)
|
||||
if m then
|
||||
a,b,c = match(str,"^(.-)\1(.-)\1(.-)\1",pos+1)
|
||||
pos = n
|
||||
return Vector(tonumber(a), tonumber(b), tonumber(c))
|
||||
else
|
||||
error("expected vector, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[9 ] = function() -- Angle
|
||||
m,n = find(str, ".-\1.-\1.-\1", pos)
|
||||
if m then
|
||||
a,b,c = match(str, "^(.-)\1(.-)\1(.-)\1",pos+1)
|
||||
pos = n
|
||||
return Angle(tonumber(a), tonumber(b), tonumber(c))
|
||||
else
|
||||
error("expected angle, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[10 ] = function() -- Table Reference
|
||||
m = find(str,"\1",pos)
|
||||
if m then
|
||||
w = sub(str,pos+1,m-1)
|
||||
pos = m
|
||||
else
|
||||
error("expected table identifier, got EOF\n")
|
||||
end
|
||||
tblref = tables[w]
|
||||
|
||||
if tblref then
|
||||
return tblref
|
||||
else
|
||||
error(format("table identifier %s points to nil\n", w))
|
||||
end
|
||||
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
|
||||
local function read_v1()
|
||||
local t = byte(str,pos+1)
|
||||
if t then
|
||||
local dt = decode_types_v1[t]
|
||||
if dt then
|
||||
pos = pos + 1
|
||||
return dt()
|
||||
else
|
||||
error(format("encountered invalid data type (%u)\n",t))
|
||||
end
|
||||
else
|
||||
error("expected value, got EOF\n")
|
||||
end
|
||||
end
|
||||
|
||||
decode_types_v1 = {
|
||||
[1 ] = function()
|
||||
error("expected value, got terminator\n")
|
||||
end,
|
||||
[2 ] = function() -- table
|
||||
local t = {}
|
||||
while true do
|
||||
if byte(str,pos+1) == 1 then
|
||||
pos = pos+1
|
||||
return t
|
||||
else
|
||||
t[read_v1()] = read_v1()
|
||||
end
|
||||
end
|
||||
end,
|
||||
[3 ] = function() -- array
|
||||
local t, i = {}, 1
|
||||
while true do
|
||||
if byte(str,pos+1) == 1 then
|
||||
pos = pos+1
|
||||
return t
|
||||
else
|
||||
t[i] = read_v1()
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end,
|
||||
[4 ] = function() -- false boolean
|
||||
return false
|
||||
end,
|
||||
[5 ] = function() -- true boolean
|
||||
return true
|
||||
end,
|
||||
[6 ] = function() -- number
|
||||
m = find(str,"\1",pos)
|
||||
if m then
|
||||
a = tonumber(sub(str,pos+1,m-1)) or 0
|
||||
pos = m
|
||||
return a
|
||||
else
|
||||
error("expected number, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[7 ] = function() -- string
|
||||
m = find(str,"\1",pos)
|
||||
if m then
|
||||
w = sub(str,pos+1,m-1)
|
||||
pos = m
|
||||
return w
|
||||
else
|
||||
error("expected string, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[8 ] = function() -- Vector
|
||||
m,n = find(str,".-\1.-\1.-\1",pos)
|
||||
if m then
|
||||
a,b,c = match(str,"^(.-)\1(.-)\1(.-)\1",pos+1)
|
||||
pos = n
|
||||
return Vector(tonumber(a), tonumber(b), tonumber(c))
|
||||
else
|
||||
error("expected vector, got EOF\n")
|
||||
end
|
||||
end,
|
||||
[9 ] = function() -- Angle
|
||||
m,n = find(str,".-\1.-\1.-\1",pos)
|
||||
if m then
|
||||
a,b,c = match(str,"^(.-)\1(.-)\1(.-)\1",pos+1)
|
||||
pos = n
|
||||
return Angle(tonumber(a), tonumber(b), tonumber(c))
|
||||
else
|
||||
error("expected angle, got EOF\n")
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function deserialize_v1(data)
|
||||
str = data
|
||||
pos = 0
|
||||
tables = {}
|
||||
return read_v1()
|
||||
end
|
||||
|
||||
local function deserialize_v2(data)
|
||||
str = data
|
||||
pos = 0
|
||||
tables = {}
|
||||
return read_v2()
|
||||
end
|
||||
|
||||
local function lzwDecode(encoded)
|
||||
local dictionary_length = 256
|
||||
local dictionary = {}
|
||||
for i = 0, 255 do
|
||||
dictionary[i] = char(i)
|
||||
end
|
||||
|
||||
local pos = 2
|
||||
local decompressed = {}
|
||||
local decompressed_length = 1
|
||||
|
||||
local index = byte(encoded)
|
||||
local word = dictionary[index]
|
||||
|
||||
decompressed[decompressed_length] = dictionary[index]
|
||||
|
||||
local entry
|
||||
local encoded_length = #encoded
|
||||
local firstbyte --of an index
|
||||
while pos <= encoded_length do
|
||||
firstbyte = byte(encoded,pos)
|
||||
if firstbyte > 252 then --now we know it's a length indicator for a multibyte index
|
||||
index = 0
|
||||
firstbyte = 256 - firstbyte
|
||||
|
||||
--[[if pos+firstbyte > encoded_length then --will test for performance impact
|
||||
error("expected index got EOF")
|
||||
end]]
|
||||
|
||||
for i = pos+firstbyte, pos+1, -1 do
|
||||
index = bit.bor(bit.lshift(index, 8), byte(encoded,i))
|
||||
end
|
||||
pos = pos + firstbyte + 1
|
||||
else
|
||||
index = firstbyte
|
||||
pos = pos + 1
|
||||
end
|
||||
entry = dictionary[index] or (word..sub(word,1,1))
|
||||
decompressed_length = decompressed_length + 1
|
||||
decompressed[decompressed_length] = entry
|
||||
dictionary[dictionary_length] = word..sub(entry,1,1)
|
||||
dictionary_length = dictionary_length + 1
|
||||
word = entry
|
||||
end
|
||||
return concat(decompressed)
|
||||
end
|
||||
|
||||
--http://en.wikipedia.org/wiki/Huffman_coding#Decompression
|
||||
|
||||
local invcodes = {[2]={[0]="\254"},[5]={[22]="\1",[11]="\2"},[6]={[13]="\7",[35]="\6",[37]="\5",[58]="\3",[31]="\8",[9]="\13",[51]="\9",[55]="\10",[57]="\4",[59]="\15"},[7]={[1]="\14",[15]="\16",[87]="\31",[89]="\30",[62]="\26",[17]="\27",[97]="\19",[19]="\43",[10]="\12",[39]="\33",[41]="\24",[82]="\40",[3]="\32",[46]="\41",[47]="\38",[94]="\25",[65]="\23",[50]="\39",[26]="\11",[7]="\28",[33]="\18",[61]="\17",[25]="\42"},[8]={[111]="\101",[162]="\29",[2]="\34",[133]="\21",[142]="\36",[5]="\20",[21]="\37",[170]="\44",[130]="\22",[66]="\35"},[9]={[241]="\121",[361]="\104",[365]="\184",[125]="\227",[373]="\198",[253]="\117",[381]="\57",[270]="\49",[413]="\80",[290]="\47",[294]="\115",[38]="\112",[429]="\74",[433]="\0",[437]="\48",[158]="\183",[453]="\107",[166]="\111",[469]="\182",[477]="\241",[45]="\86",[489]="\69",[366]="\100",[497]="\61",[509]="\76",[49]="\53",[390]="\78",[279]="\196",[283]="\70",[414]="\98",[53]="\55",[422]="\109",[233]="\79",[349]="\89",[369]="\52",[14]="\105",[238]="\56",[319]="\162",[323]="\83",[327]="\63",[458]="\65",[335]="\231",[339]="\225",[337]="\114",[347]="\193",[493]="\139",[23]="\209",[359]="\250",[490]="\68",[42]="\54",[63]="\91",[286]="\97",[254]="\50",[510]="\108",[109]="\73",[67]="\103",[255]="\122",[69]="\170",[70]="\110",[407]="\176",[411]="\119",[110]="\120",[83]="\146",[149]="\163",[151]="\224",[85]="\51",[155]="\177",[79]="\251",[27]="\118",[447]="\159",[451]="\228",[455]="\175",[383]="\174",[463]="\243",[467]="\157",[173]="\210",[475]="\167",[177]="\84",[90]="\45",[487]="\206",[93]="\226",[495]="\245",[207]="\64",[127]="\147",[191]="\155",[511]="\153",[195]="\208",[197]="\85",[199]="\178",[181]="\82",[102]="\116",[103]="\71",[285]="\144",[105]="\102",[211]="\199",[213]="\123",[301]="\66",[305]="\46",[219]="\137",[81]="\67",[91]="\88",[157]="\130",[325]="\95",[29]="\58",[231]="\201",[117]="\99",[341]="\222",[237]="\77",[239]="\211",[71]="\223"},[10]={[710]="\149",[245]="\60",[742]="\172",[774]="\81",[134]="\151",[917]="\145",[274]="\216",[405]="\242",[146]="\194",[838]="\246",[298]="\248",[870]="\189",[1013]="\150",[894]="\190",[326]="\244",[330]="\166",[334]="\217",[465]="\179",[346]="\59",[354]="\180",[966]="\212",[974]="\143",[370]="\148",[998]="\154",[625]="\138",[382]="\161",[194]="\141",[198]="\126",[402]="\96",[206]="\185",[586]="\129",[721]="\187",[610]="\135",[618]="\181",[626]="\72",[226]="\62",[454]="\127",[658]="\113",[462]="\164",[234]="\214",[474]="\140",[242]="\106",[714]="\188",[730]="\87",[498]="\237",[746]="\125",[754]="\229",[786]="\128",[202]="\93",[18]="\255",[810]="\173",[846]="\131",[74]="\192",[842]="\142",[977]="\252",[858]="\235",[78]="\134",[874]="\234",[882]="\90",[646]="\92",[1006]="\160",[126]="\165",[914]="\221",[718]="\94",[738]="\238",[638]="\197",[482]="\230",[34]="\220",[962]="\133",[6]="\213",[706]="\219",[986]="\171",[994]="\233",[866]="\200",[1010]="\247",[98]="\169",[518]="\236",[494]="\207",[230]="\205",[542]="\191",[501]="\202",[530]="\203",[450]="\204",[209]="\158",[106]="\186",[590]="\136",[218]="\232",[733]="\124",[309]="\168",[221]="\152",[757]="\240",[113]="\215",[114]="\156",[362]="\239",[486]="\132",[358]="\249",[262]="\75",[30]="\218",[821]="\195",[546]="\253"}}
|
||||
|
||||
local function huffmanDecode(encoded)
|
||||
|
||||
local h1,h2,h3 = byte(encoded, 1, 3)
|
||||
|
||||
if (not h3) or (#encoded < 4) then
|
||||
error("invalid input")
|
||||
end
|
||||
|
||||
local original_length = bit.bor(bit.lshift(h3,16), bit.lshift(h2,8), h1)
|
||||
local encoded_length = #encoded+1
|
||||
local decoded = {}
|
||||
local decoded_length = 0
|
||||
local buffer = 0
|
||||
local buffer_length = 0
|
||||
local code
|
||||
local code_len = 2
|
||||
local temp
|
||||
local pos = 4
|
||||
|
||||
while decoded_length < original_length do
|
||||
if code_len <= buffer_length then
|
||||
temp = invcodes[code_len]
|
||||
code = bit.band(buffer, bit.lshift(1, code_len)-1)
|
||||
if temp and temp[code] then --most of the time temp is nil
|
||||
decoded_length = decoded_length + 1
|
||||
decoded[decoded_length] = temp[code]
|
||||
buffer = bit.rshift(buffer, code_len)
|
||||
buffer_length = buffer_length - code_len
|
||||
code_len = 2
|
||||
else
|
||||
code_len = code_len + 1
|
||||
if code_len > 10 then
|
||||
error("malformed code")
|
||||
end
|
||||
end
|
||||
else
|
||||
buffer = bit.bor(buffer, bit.lshift(byte(encoded, pos), buffer_length))
|
||||
buffer_length = buffer_length + 8
|
||||
pos = pos + 1
|
||||
if pos > encoded_length then
|
||||
error("malformed code")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return concat(decoded)
|
||||
end
|
||||
|
||||
local function invEscapeSub(str)
|
||||
local escseq,body = match(str,"^(.-)\n(.-)$")
|
||||
|
||||
if not escseq then error("invalid input") end
|
||||
|
||||
return gsub(body,escseq,"\26")
|
||||
end
|
||||
|
||||
local dictionary
|
||||
local subtables
|
||||
|
||||
local function deserializeChunk(chunk)
|
||||
|
||||
local ctype,val = byte(chunk),sub(chunk,3)
|
||||
|
||||
if ctype == 89 then return dictionary[ val ]
|
||||
elseif ctype == 86 then
|
||||
local a,b,c = match(val,"^(.-),(.-),(.+)$")
|
||||
return Vector( tonumber(a), tonumber(b), tonumber(c) )
|
||||
elseif ctype == 65 then
|
||||
local a,b,c = match(val,"^(.-),(.-),(.+)$")
|
||||
return Angle( tonumber(a), tonumber(b), tonumber(c) )
|
||||
elseif ctype == 84 then
|
||||
local t = {}
|
||||
local tv = subtables[val]
|
||||
if not tv then
|
||||
tv = {}
|
||||
subtables[ val ] = tv
|
||||
end
|
||||
tv[#tv+1] = t
|
||||
return t
|
||||
elseif ctype == 78 then return tonumber(val)
|
||||
elseif ctype == 83 then return gsub(sub(val,2,-2),"»",";")
|
||||
elseif ctype == 66 then return val == "t"
|
||||
elseif ctype == 80 then return 1
|
||||
end
|
||||
|
||||
error(format("AD1 deserialization failed: invalid chunk (%u:%s)",ctype,val))
|
||||
|
||||
end
|
||||
|
||||
local function deserializeAD1(dupestring)
|
||||
|
||||
dupestring = dupestring:Replace("\r\n", "\n")
|
||||
local header, extraHeader, dupeBlock, dictBlock = dupestring:match("%[Info%]\n(.+)\n%[More Information%]\n(.+)\n%[Save%]\n(.+)\n%[Dict%]\n(.+)")
|
||||
|
||||
if not header then
|
||||
error("unknown duplication format")
|
||||
end
|
||||
|
||||
local info = {}
|
||||
for k,v in header:gmatch("([^\n:]+):([^\n]+)") do
|
||||
info[k] = v
|
||||
end
|
||||
|
||||
local moreinfo = {}
|
||||
for k,v in extraHeader:gmatch("([^\n:]+):([^\n]+)") do
|
||||
moreinfo[k] = v
|
||||
end
|
||||
|
||||
dictionary = {}
|
||||
for k,v in dictBlock:gmatch("(.-):\"(.-)\"\n") do
|
||||
dictionary[k] = v
|
||||
end
|
||||
|
||||
local dupe = {}
|
||||
for key,block in dupeBlock:gmatch("([^\n:]+):([^\n]+)") do
|
||||
|
||||
local tables = {}
|
||||
subtables = {}
|
||||
local head
|
||||
|
||||
for id,chunk in block:gmatch('(%w+){(.-)}') do
|
||||
|
||||
--check if this table is the trunk
|
||||
if byte(id) == 72 then
|
||||
id = sub(id,2)
|
||||
head = id
|
||||
end
|
||||
|
||||
tables[id] = {}
|
||||
|
||||
for kv in gmatch(chunk,'[^;]+') do
|
||||
|
||||
local k,v = match(kv,'(.-)=(.+)')
|
||||
|
||||
if k then
|
||||
k = deserializeChunk( k )
|
||||
v = deserializeChunk( v )
|
||||
|
||||
tables[id][k] = v
|
||||
else
|
||||
v = deserializeChunk( kv )
|
||||
local tid = tables[id]
|
||||
tid[#tid+1]=v
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
--Restore table references
|
||||
for id,tbls in pairs( subtables ) do
|
||||
for _,tbl in pairs( tbls ) do
|
||||
if not tables[id] then error("attempt to reference a nonexistent table") end
|
||||
for k,v in pairs(tables[id]) do
|
||||
tbl[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
dupe[key] = tables[ head ]
|
||||
|
||||
end
|
||||
|
||||
return dupe, info, moreinfo
|
||||
|
||||
end
|
||||
|
||||
--seperates the header and body and converts the header to a table
|
||||
local function getInfo(str)
|
||||
local last = str:find("\2")
|
||||
if not last then
|
||||
error("attempt to read AD2 file with malformed info block error 1")
|
||||
end
|
||||
local info = {}
|
||||
local ss = str:sub(1,last-1)
|
||||
for k,v in ss:gmatch("(.-)\1(.-)\1") do
|
||||
info[k] = v
|
||||
end
|
||||
if info.check ~= "\r\n\t\n" then
|
||||
if info.check == "\10\9\10" then
|
||||
error("detected AD2 file corrupted in file transfer (newlines homogenized)(when using FTP, transfer AD2 files in image/binary mode, not ASCII/text mode)")
|
||||
else
|
||||
error("attempt to read AD2 file with malformed info block error 2")
|
||||
end
|
||||
end
|
||||
return info, str:sub(last+2)
|
||||
end
|
||||
|
||||
--decoders for individual versions go here
|
||||
local versions = {}
|
||||
|
||||
versions[2] = function(encodedDupe)
|
||||
encodedDupe = encodedDupe:Replace("\r\r\n\t\r\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n\t\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n", "\n")
|
||||
encodedDupe = encodedDupe:Replace("\t\t\t\t", "\r\n\t\n")
|
||||
local info, dupestring = getInfo(encodedDupe:sub(7))
|
||||
return deserialize_v2(
|
||||
lzwDecode(
|
||||
huffmanDecode(
|
||||
invEscapeSub(dupestring)
|
||||
)
|
||||
)
|
||||
), info
|
||||
end
|
||||
|
||||
versions[1] = function(encodedDupe)
|
||||
encodedDupe = encodedDupe:Replace("\r\r\n\t\r\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n\t\n", "\t\t\t\t")
|
||||
encodedDupe = encodedDupe:Replace("\r\n", "\n")
|
||||
encodedDupe = encodedDupe:Replace("\t\t\t\t", "\r\n\t\n")
|
||||
local info, dupestring = getInfo(encodedDupe:sub(7))
|
||||
return deserialize_v1(
|
||||
lzwDecode(
|
||||
huffmanDecode(
|
||||
invEscapeSub(dupestring)
|
||||
)
|
||||
)
|
||||
), info
|
||||
end
|
||||
|
||||
versions[0] = deserializeAD1
|
||||
|
||||
AdvDupe2.LegacyDecoders = versions
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
--Save a file to the client
|
||||
local function SaveFile(ply, cmd, args)
|
||||
if(not ply.AdvDupe2 or not ply.AdvDupe2.Entities or next(ply.AdvDupe2.Entities)==nil)then AdvDupe2.Notify(ply,"Duplicator is empty, nothing to save.", NOTIFY_ERROR) return end
|
||||
if(not game.SinglePlayer() and CurTime()-(ply.AdvDupe2.FileMod or 0) < 0)then
|
||||
AdvDupe2.Notify(ply,"Cannot save at the moment. Please Wait...", NOTIFY_ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if(ply.AdvDupe2.Pasting || ply.AdvDupe2.Downloading)then
|
||||
AdvDupe2.Notify(ply,"Advanced Duplicator 2 is busy.",NOTIFY_ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
ply.AdvDupe2.FileMod = CurTime()+tonumber(GetConVarString("AdvDupe2_FileModificationDelay")+2)
|
||||
|
||||
local name = string.Explode("/", args[1])
|
||||
ply.AdvDupe2.Name = name[#name]
|
||||
|
||||
net.Start("AdvDupe2_SetDupeInfo")
|
||||
net.WriteString(ply.AdvDupe2.Name)
|
||||
net.WriteString(ply:Nick())
|
||||
net.WriteString(os.date("%d %B %Y"))
|
||||
net.WriteString(os.date("%I:%M %p"))
|
||||
net.WriteString("")
|
||||
net.WriteString(args[2] or "")
|
||||
net.WriteString(table.Count(ply.AdvDupe2.Entities))
|
||||
net.WriteString(#ply.AdvDupe2.Constraints)
|
||||
net.Send(ply)
|
||||
|
||||
local Tab = {Entities = ply.AdvDupe2.Entities, Constraints = ply.AdvDupe2.Constraints, HeadEnt = ply.AdvDupe2.HeadEnt, Description=args[2]}
|
||||
|
||||
AdvDupe2.Encode( Tab, AdvDupe2.GenerateDupeStamp(ply), function(data)
|
||||
AdvDupe2.SendToClient(ply, data, false)
|
||||
end)
|
||||
end
|
||||
concommand.Add("AdvDupe2_SaveFile", SaveFile)
|
||||
|
||||
function AdvDupe2.SendToClient(ply, data, autosave)
|
||||
if(not IsValid(ply))then return end
|
||||
if #data > AdvDupe2.MaxDupeSize then
|
||||
AdvDupe2.Notify(ply,"Copied duplicator filesize is too big!",NOTIFY_ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
ply.AdvDupe2.Downloading = true
|
||||
AdvDupe2.InitProgressBar(ply,"Saving:")
|
||||
|
||||
net.Start("AdvDupe2_ReceiveFile")
|
||||
net.WriteBool(autosave)
|
||||
net.WriteStream(data, function()
|
||||
ply.AdvDupe2.Downloading = false
|
||||
end)
|
||||
net.Send(ply)
|
||||
end
|
||||
|
||||
function AdvDupe2.LoadDupe(ply,success,dupe,info,moreinfo)
|
||||
if(not IsValid(ply))then return end
|
||||
|
||||
if not success then
|
||||
AdvDupe2.Notify(ply,"Could not open "..dupe,NOTIFY_ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if(not game.SinglePlayer())then
|
||||
if(tonumber(GetConVarString("AdvDupe2_MaxConstraints"))~=0 and #dupe["Constraints"]>tonumber(GetConVarString("AdvDupe2_MaxConstraints")))then
|
||||
AdvDupe2.Notify(ply,"Amount of constraints is greater than "..GetConVarString("AdvDupe2_MaxConstraints"),NOTIFY_ERROR)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
ply.AdvDupe2.Entities = {}
|
||||
ply.AdvDupe2.Constraints = {}
|
||||
ply.AdvDupe2.HeadEnt={}
|
||||
ply.AdvDupe2.Revision = info.revision
|
||||
|
||||
if(info.ad1)then
|
||||
|
||||
ply.AdvDupe2.HeadEnt.Index = tonumber(moreinfo.Head)
|
||||
local spx,spy,spz = moreinfo.StartPos:match("^(.-),(.-),(.+)$")
|
||||
ply.AdvDupe2.HeadEnt.Pos = Vector(tonumber(spx) or 0, tonumber(spy) or 0, tonumber(spz) or 0)
|
||||
local z = (tonumber(moreinfo.HoldPos:match("^.-,.-,(.+)$")) or 0)*-1
|
||||
ply.AdvDupe2.HeadEnt.Z = z
|
||||
ply.AdvDupe2.HeadEnt.Pos.Z = ply.AdvDupe2.HeadEnt.Pos.Z + z
|
||||
local Pos
|
||||
local Ang
|
||||
for k,v in pairs(dupe["Entities"])do
|
||||
Pos = nil
|
||||
Ang = nil
|
||||
if(v.SavedParentIdx)then
|
||||
if(not v.BuildDupeInfo)then v.BuildDupeInfo = {} end
|
||||
v.BuildDupeInfo.DupeParentID = v.SavedParentIdx
|
||||
Pos = v.LocalPos*1
|
||||
Ang = v.LocalAngle*1
|
||||
end
|
||||
for i,p in pairs(v.PhysicsObjects)do
|
||||
p.Pos = Pos or (p.LocalPos*1)
|
||||
p.Pos.Z = p.Pos.Z - z
|
||||
p.Angle = Ang or (p.LocalAngle*1)
|
||||
p.LocalPos = nil
|
||||
p.LocalAngle = nil
|
||||
p.Frozen = not p.Frozen -- adv dupe 2 does this wrong way
|
||||
end
|
||||
v.LocalPos = nil
|
||||
v.LocalAngle = nil
|
||||
end
|
||||
|
||||
ply.AdvDupe2.Entities = dupe["Entities"]
|
||||
ply.AdvDupe2.Constraints = dupe["Constraints"]
|
||||
|
||||
else
|
||||
ply.AdvDupe2.Entities = dupe["Entities"]
|
||||
ply.AdvDupe2.Constraints = dupe["Constraints"]
|
||||
ply.AdvDupe2.HeadEnt = dupe["HeadEnt"]
|
||||
end
|
||||
AdvDupe2.ResetOffsets(ply, true)
|
||||
end
|
||||
|
||||
local function AdvDupe2_ReceiveFile(len, ply)
|
||||
if not IsValid(ply) then return end
|
||||
if not ply.AdvDupe2 then ply.AdvDupe2 = {} end
|
||||
|
||||
ply.AdvDupe2.Name = string.match(net.ReadString(), "([%w_ ]+)") or "Advanced Duplication"
|
||||
|
||||
local stream = net.ReadStream(ply, function(data)
|
||||
if data then
|
||||
AdvDupe2.LoadDupe(ply, AdvDupe2.Decode(data))
|
||||
else
|
||||
AdvDupe2.Notify(ply, "Duplicator Upload Failed!", NOTIFY_ERROR, 5)
|
||||
end
|
||||
ply.AdvDupe2.Uploading = false
|
||||
end)
|
||||
|
||||
if ply.AdvDupe2.Uploading then
|
||||
if stream then
|
||||
stream:Remove()
|
||||
end
|
||||
AdvDupe2.Notify(ply, "Duplicator is Busy!", NOTIFY_ERROR, 5)
|
||||
elseif stream then
|
||||
ply.AdvDupe2.Uploading = true
|
||||
AdvDupe2.InitProgressBar(ply, "Uploading: ")
|
||||
end
|
||||
end
|
||||
net.Receive("AdvDupe2_ReceiveFile", AdvDupe2_ReceiveFile)
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
util.AddNetworkString("AdvDupe2_SendGhosts")
|
||||
util.AddNetworkString("AdvDupe2_AddGhost")
|
||||
|
||||
function AdvDupe2.SendGhost(ply, AddOne)
|
||||
net.Start("AdvDupe2_AddGhost")
|
||||
net.WriteString(AddOne.Model)
|
||||
net.WriteInt(#AddOne.PhysicsObjects, 8)
|
||||
for i=0, #AddOne.PhysicsObjects do
|
||||
net.WriteAngle(AddOne.PhysicsObjects[i].Angle)
|
||||
net.WriteVector(AddOne.PhysicsObjects[i].Pos)
|
||||
end
|
||||
net.Send(ply)
|
||||
end
|
||||
|
||||
function AdvDupe2.SendGhosts(ply)
|
||||
if(not ply.AdvDupe2.Entities)then return end
|
||||
|
||||
local cache = {}
|
||||
local temp = {}
|
||||
local mdls = {}
|
||||
local cnt = 1
|
||||
local add = true
|
||||
local head
|
||||
|
||||
for k,v in pairs(ply.AdvDupe2.Entities)do
|
||||
temp[cnt] = v
|
||||
for i=1,#cache do
|
||||
if(cache[i]==v.Model)then
|
||||
mdls[cnt] = i
|
||||
add=false
|
||||
break
|
||||
end
|
||||
end
|
||||
if(add)then
|
||||
mdls[cnt] = table.insert(cache, v.Model)
|
||||
else
|
||||
add = true
|
||||
end
|
||||
if(k==ply.AdvDupe2.HeadEnt.Index)then
|
||||
head = cnt
|
||||
end
|
||||
cnt = cnt+1
|
||||
end
|
||||
|
||||
if(!head)then
|
||||
AdvDupe2.Notify(ply, "Invalid head entity for ghosts.", NOTIFY_ERROR);
|
||||
return
|
||||
end
|
||||
|
||||
net.Start("AdvDupe2_SendGhosts")
|
||||
net.WriteInt(head, 16)
|
||||
net.WriteFloat(ply.AdvDupe2.HeadEnt.Z)
|
||||
net.WriteVector(ply.AdvDupe2.HeadEnt.Pos)
|
||||
net.WriteInt(#cache, 16)
|
||||
for i=1,#cache do
|
||||
net.WriteString(cache[i])
|
||||
end
|
||||
net.WriteInt(cnt-1, 16)
|
||||
for i=1, #temp do
|
||||
net.WriteInt(mdls[i], 16)
|
||||
net.WriteInt(#temp[i].PhysicsObjects, 8)
|
||||
for k=0, #temp[i].PhysicsObjects do
|
||||
net.WriteAngle(temp[i].PhysicsObjects[k].Angle)
|
||||
net.WriteVector(temp[i].PhysicsObjects[k].Pos)
|
||||
end
|
||||
end
|
||||
net.Send(ply)
|
||||
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
--[[
|
||||
Title: Miscellaneous
|
||||
|
||||
Desc: Contains miscellaneous (serverside) things AD2 needs to function that don't fit anywhere else.
|
||||
|
||||
Author: TB
|
||||
|
||||
Version: 1.0
|
||||
]]
|
||||
|
||||
--[[
|
||||
Name: SavePositions
|
||||
Desc: Save the position of the entities to prevent sagging on dupe.
|
||||
Params: <entity> Constraint
|
||||
Returns: nil
|
||||
]]
|
||||
|
||||
local function SavePositions( Constraint )
|
||||
|
||||
if IsValid(Constraint) then
|
||||
|
||||
if Constraint.BuildDupeInfo then return end
|
||||
local BuildDupeInfo = {}
|
||||
Constraint.BuildDupeInfo = BuildDupeInfo
|
||||
|
||||
local Ent1, Ent2
|
||||
if IsValid(Constraint.Ent) then
|
||||
if Constraint.Ent:GetPhysicsObjectCount()>1 then
|
||||
BuildDupeInfo.Ent1Ang = Constraint.Ent:GetAngles()
|
||||
else
|
||||
BuildDupeInfo.Ent1Ang = Constraint.Ent:GetPhysicsObject():GetAngles()
|
||||
end
|
||||
end
|
||||
|
||||
if IsValid(Constraint.Ent1) then
|
||||
if Constraint.Ent1:GetPhysicsObjectCount()>1 then
|
||||
local Bone = Constraint.Ent1:GetPhysicsObjectNum(Constraint.Bone1)
|
||||
BuildDupeInfo.Ent1Ang = Constraint.Ent1:GetAngles()
|
||||
BuildDupeInfo.Ent1Pos = Constraint.Ent1:GetPos()
|
||||
BuildDupeInfo.Bone1 = Constraint.Bone1
|
||||
BuildDupeInfo.Bone1Pos = Bone:GetPos() - Constraint.Ent1:GetPos()
|
||||
BuildDupeInfo.Bone1Angle = Bone:GetAngles()
|
||||
else
|
||||
local Bone = Constraint.Ent1:GetPhysicsObject()
|
||||
BuildDupeInfo.Ent1Ang = Bone:GetAngles()
|
||||
BuildDupeInfo.Ent1Pos = Bone:GetPos()
|
||||
end
|
||||
|
||||
if IsValid(Constraint.Ent2) then
|
||||
if Constraint.Ent2:GetPhysicsObjectCount()>1 then
|
||||
local Bone = Constraint.Ent2:GetPhysicsObjectNum(Constraint.Bone2)
|
||||
BuildDupeInfo.EntityPos = BuildDupeInfo.Ent1Pos - Constraint.Ent2:GetPos()
|
||||
BuildDupeInfo.Ent2Ang = Constraint.Ent2:GetAngles()
|
||||
BuildDupeInfo.Bone2 = Constraint.Bone2
|
||||
BuildDupeInfo.Bone2Pos = Bone:GetPos() - Constraint.Ent2:GetPos()
|
||||
BuildDupeInfo.Bone2Angle = Bone:GetAngles()
|
||||
else
|
||||
local Bone = Constraint.Ent2:GetPhysicsObject()
|
||||
BuildDupeInfo.EntityPos = BuildDupeInfo.Ent1Pos - Bone:GetPos()
|
||||
BuildDupeInfo.Ent2Ang = Bone:GetAngles()
|
||||
end
|
||||
elseif IsValid(Constraint.Ent4) then
|
||||
if Constraint.Ent4:GetPhysicsObjectCount()>1 then
|
||||
local Bone = Constraint.Ent4:GetPhysicsObjectNum(Constraint.Bone4)
|
||||
BuildDupeInfo.Bone2 = Constraint.Bone4
|
||||
BuildDupeInfo.EntityPos = BuildDupeInfo.Ent1Pos - Constraint.Ent4:GetPos()
|
||||
BuildDupeInfo.Ent2Ang = Constraint.Ent4:GetAngles()
|
||||
BuildDupeInfo.Bone2Pos = Bone:GetPos() - Constraint.Ent4:GetPos()
|
||||
BuildDupeInfo.Bone2Angle = Bone:GetAngles()
|
||||
else
|
||||
local Bone = Constraint.Ent4:GetPhysicsObject()
|
||||
BuildDupeInfo.EntityPos = BuildDupeInfo.Ent1Pos - Bone:GetPos()
|
||||
BuildDupeInfo.Ent2Ang = Bone:GetAngles()
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
local function monitorConstraint(name)
|
||||
local oldFunc = constraint[name]
|
||||
constraint[name] = function(...)
|
||||
local Constraint, b, c = oldFunc(...)
|
||||
if Constraint and Constraint:IsValid() then
|
||||
SavePositions(Constraint)
|
||||
end
|
||||
return Constraint, b, c
|
||||
end
|
||||
end
|
||||
monitorConstraint("AdvBallsocket")
|
||||
monitorConstraint("Axis")
|
||||
monitorConstraint("Ballsocket")
|
||||
monitorConstraint("Elastic")
|
||||
monitorConstraint("Hydraulic")
|
||||
monitorConstraint("Keepupright")
|
||||
monitorConstraint("Motor")
|
||||
monitorConstraint("Muscle")
|
||||
monitorConstraint("Pulley")
|
||||
monitorConstraint("Rope")
|
||||
monitorConstraint("Slider")
|
||||
monitorConstraint("Weld")
|
||||
monitorConstraint("Winch")
|
||||
@@ -0,0 +1,145 @@
|
||||
include("shared.lua")
|
||||
|
||||
surface.CreateFont( "InfoRUS2", { font = "Enhanced Dot Digital-7", extended = true, size = 90, weight = 800, antialias = true })
|
||||
surface.CreateFont( "InfoRUS3", { font = "Enhanced Dot Digital-7", extended = true, size = 50, weight = 800, antialias = true })
|
||||
|
||||
local font = "InfoRUS2"
|
||||
|
||||
local sizetable = {
|
||||
[3] = {350, 0.5},
|
||||
[4] = {470, -11.5},
|
||||
[5] = {590, -11.5},
|
||||
[6] = {710, 0.5},
|
||||
[7] = {830, 0.5},
|
||||
[8] = {950, 0.5},
|
||||
}
|
||||
|
||||
function ENT:Initialize()
|
||||
|
||||
self.OldWide = self:GetWide()
|
||||
|
||||
self.frame = vgui.Create( "DPanel" )
|
||||
self.frame:SetSize( sizetable[self:GetWide()][1], 120 )
|
||||
self.frame.Text = self:GetText()
|
||||
self.frame.Type = self:GetType()
|
||||
self.frame.col = self:GetTColor()
|
||||
self.frame.damage = 0
|
||||
self.frame.appr = nil
|
||||
self.frame.FX = self:GetFX()
|
||||
self.frame.On = self:GetOn()
|
||||
self.frame.alfa = 0
|
||||
self.frame.speed = self:GetSpeed()
|
||||
self.frame:SetPaintedManually( true )
|
||||
self.frame.Paint = function(self,w,h)
|
||||
|
||||
if self.On <= 0 then
|
||||
if self.alfa < 1 then return end
|
||||
self.alfa = Lerp(FrameTime() * 5,self.alfa,0)
|
||||
else
|
||||
if self.FX > 0 then
|
||||
self.alfa = math.random(100,220)
|
||||
else
|
||||
self.alfa = 255
|
||||
end
|
||||
end
|
||||
|
||||
surface.DisableClipping( false )
|
||||
surface.SetFont(font)
|
||||
local ww,hh = surface.GetTextSize(self.Text)
|
||||
local multiplier = self.speed * 100
|
||||
|
||||
self.static = false
|
||||
|
||||
if self.damage < CurTime() and self.On then
|
||||
if self.Type == 1 then
|
||||
|
||||
local xs = (math.fmod(SysTime() * multiplier,w+ww)) - ww
|
||||
|
||||
draw.DrawText(self.Text,font,xs,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
elseif self.Type == 2 then
|
||||
|
||||
if !self.appr or self.appr > ww then
|
||||
self.appr = -w
|
||||
else
|
||||
self.appr = math.Approach(self.appr, ww+w, FrameTime() * multiplier)
|
||||
end
|
||||
|
||||
draw.DrawText(self.Text,font,self.appr * -1,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
else
|
||||
if !self.appr then
|
||||
self.appr = 0
|
||||
end
|
||||
|
||||
if w > ww then
|
||||
if self.Type == 3 then
|
||||
if self.appr < w-ww and !self.refl then
|
||||
self.appr = math.Approach(self.appr, ww+w, FrameTime() * multiplier)
|
||||
else
|
||||
if self.appr <= 0 then
|
||||
self.refl = nil
|
||||
else
|
||||
self.refl = true
|
||||
self.appr = math.Approach(self.appr, 0, FrameTime() * multiplier)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.static = true
|
||||
end
|
||||
else
|
||||
if self.appr > w-ww-50 and !self.refl then
|
||||
self.appr = math.Approach(self.appr, w-ww-50, FrameTime() * multiplier)
|
||||
else
|
||||
if self.appr >= 50 then
|
||||
self.refl = nil
|
||||
else
|
||||
self.refl = true
|
||||
self.appr = math.Approach(self.appr, 50, FrameTime() * multiplier)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.static then
|
||||
draw.DrawText(self.Text,font,w/2,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),1)
|
||||
else
|
||||
draw.DrawText(self.Text,font,self.appr,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
end
|
||||
end
|
||||
else
|
||||
draw.DrawText(self.Text,font,math.random(0,w-ww),10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, math.random(0,255)),0)
|
||||
end
|
||||
surface.DisableClipping( true )
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Draw()
|
||||
|
||||
self:DrawModel()
|
||||
|
||||
if self.frame then
|
||||
self.frame.Text = self:GetText()
|
||||
self.frame.Type = self:GetType()
|
||||
self.frame.col = self:GetTColor()
|
||||
self.frame.FX = self:GetFX()
|
||||
self.frame.On = self:GetOn()
|
||||
self.frame.damage = self:GetNWInt("LastDamaged")
|
||||
self.frame.speed = self:GetSpeed()
|
||||
end
|
||||
|
||||
local Pos = self:GetPos()
|
||||
local Ang = self:GetAngles()
|
||||
local hight = 12
|
||||
|
||||
if self.OldWide != self:GetWide() then
|
||||
self.frame:SetSize( sizetable[self:GetWide()][1], 120 )
|
||||
self.OldWide = self:GetWide()
|
||||
end
|
||||
|
||||
if self:GetWide() == 3 then
|
||||
hight = 6
|
||||
end
|
||||
|
||||
cam.Start3D2D(Pos + Ang:Up() * 1.1 - Ang:Right() * hight + Ang:Forward() * sizetable[self:GetWide()][2], Ang, 0.1)
|
||||
self.frame:PaintManual()
|
||||
cam.End3D2D()
|
||||
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
|
||||
util.AddNetworkString("LEDDamaged")
|
||||
|
||||
function ENT:Initialize()
|
||||
|
||||
self:SetText("Made by Mac with <3")
|
||||
self:SetTColor(Vector(2.55,2,0))
|
||||
self:SetType(1)
|
||||
self:SetSpeed(1.5)
|
||||
self:SetWide(6)
|
||||
self:SetFX(1)
|
||||
self:SetOn(1)
|
||||
|
||||
self:SetModel("models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMaterial("phoenix_storms/mat/mat_phx_carbonfiber")
|
||||
self:SetColor(Color(0,0,0))
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
self:SetUseType(SIMPLE_USE)
|
||||
self:SetNWInt("LastDamaged", 0)
|
||||
local phys = self:GetPhysicsObject()
|
||||
self.nodupe = true
|
||||
self.ShareGravgun = true
|
||||
|
||||
phys:Wake()
|
||||
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
|
||||
if self:GetModel() != "models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl" then
|
||||
self:SetModel("models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl")
|
||||
end
|
||||
|
||||
self:NextThink( CurTime() + 1 )
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage(data)
|
||||
|
||||
if data:GetDamagePosition() then
|
||||
self:EmitSound("ambient/energy/spark".. math.random(1,6) ..".wav")
|
||||
local effectdata = EffectData()
|
||||
effectdata:SetOrigin( data:GetDamagePosition() )
|
||||
util.Effect( "StunstickImpact", effectdata )
|
||||
self:SetNWInt("LastDamaged", math.Round(CurTime()+2))
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
ENT.PrintName = "Spawned Sign"
|
||||
ENT.Author = "Mac"
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminSpawnable = true
|
||||
|
||||
function ENT:SetupDataTables()
|
||||
self:NetworkVar("String", 0, "Text")
|
||||
self:NetworkVar("Vector", 0, "TColor")
|
||||
self:NetworkVar("Int", 0, "Type")
|
||||
self:NetworkVar("Int", 1, "Speed")
|
||||
self:NetworkVar("Int", 2, "Wide")
|
||||
self:NetworkVar("Int", 3, "On")
|
||||
self:NetworkVar("Int", 4, "FX")
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
include("shared.lua")
|
||||
|
||||
local font = "InfoRUS2"
|
||||
|
||||
local sizetable = {
|
||||
[3] = {350, 0.5},
|
||||
[4] = {470, -11.5},
|
||||
[5] = {590, -11.5},
|
||||
[6] = {710, 0.5},
|
||||
[7] = {830, 0.5},
|
||||
[8] = {950, 0.5},
|
||||
}
|
||||
|
||||
function ENT:Initialize()
|
||||
|
||||
self.OldWide = self:GetWide()
|
||||
|
||||
self.frame = vgui.Create( "DPanel" )
|
||||
self.frame:SetSize( sizetable[self:GetWide()][1], 120 )
|
||||
self.frame.Text = self:GetText()
|
||||
self.frame.Type = self:GetType()
|
||||
self.frame.col = self:GetTColor()
|
||||
self.frame.damage = 0
|
||||
self.frame.appr = nil
|
||||
self.frame.FX = self:GetFX()
|
||||
self.frame.On = self:GetOn()
|
||||
self.frame.alfa = 0
|
||||
self.frame.speed = self:GetSpeed()
|
||||
self.frame:SetPaintedManually( true )
|
||||
self.frame.Paint = function(self,w,h)
|
||||
|
||||
if self.On <= 0 then
|
||||
if self.alfa < 1 then return end
|
||||
self.alfa = Lerp(FrameTime() * 5,self.alfa,0)
|
||||
else
|
||||
if self.FX > 0 then
|
||||
self.alfa = math.random(100,220)
|
||||
else
|
||||
self.alfa = 255
|
||||
end
|
||||
end
|
||||
|
||||
surface.DisableClipping( false )
|
||||
surface.SetFont(font)
|
||||
local ww,hh = surface.GetTextSize(self.Text)
|
||||
local multiplier = self.speed * 100
|
||||
|
||||
self.static = false
|
||||
|
||||
if self.damage < CurTime() and self.On then
|
||||
if self.Type == 1 then
|
||||
|
||||
local xs = (math.fmod(SysTime() * multiplier,w+ww)) - ww
|
||||
|
||||
draw.DrawText(self.Text,font,xs,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
elseif self.Type == 2 then
|
||||
|
||||
if !self.appr or self.appr > ww then
|
||||
self.appr = -w
|
||||
else
|
||||
self.appr = math.Approach(self.appr, ww+w, FrameTime() * multiplier)
|
||||
end
|
||||
|
||||
draw.DrawText(self.Text,font,self.appr * -1,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
else
|
||||
if !self.appr then
|
||||
self.appr = 0
|
||||
end
|
||||
|
||||
if w > ww then
|
||||
if self.Type == 3 then
|
||||
if self.appr < w-ww and !self.refl then
|
||||
self.appr = math.Approach(self.appr, ww+w, FrameTime() * multiplier)
|
||||
else
|
||||
if self.appr <= 0 then
|
||||
self.refl = nil
|
||||
else
|
||||
self.refl = true
|
||||
self.appr = math.Approach(self.appr, 0, FrameTime() * multiplier)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.static = true
|
||||
end
|
||||
else
|
||||
if self.appr > w-ww-50 and !self.refl then
|
||||
self.appr = math.Approach(self.appr, w-ww-50, FrameTime() * multiplier)
|
||||
else
|
||||
if self.appr >= 50 then
|
||||
self.refl = nil
|
||||
else
|
||||
self.refl = true
|
||||
self.appr = math.Approach(self.appr, 50, FrameTime() * multiplier)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if self.static then
|
||||
draw.DrawText(self.Text,font,w/2,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),1)
|
||||
else
|
||||
draw.DrawText(self.Text,font,self.appr,10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, self.alfa),0)
|
||||
end
|
||||
end
|
||||
else
|
||||
draw.DrawText(self.Text,font,math.random(0,w-ww),10,Color(self.col.x * 100, self.col.y * 100, self.col.z * 100, math.random(0,255)),0)
|
||||
end
|
||||
surface.DisableClipping( true )
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Draw()
|
||||
|
||||
self:DrawModel()
|
||||
|
||||
if self.frame then
|
||||
self.frame.Text = self:GetText()
|
||||
self.frame.Type = self:GetType()
|
||||
self.frame.col = self:GetTColor()
|
||||
self.frame.FX = self:GetFX()
|
||||
self.frame.On = self:GetOn()
|
||||
self.frame.damage = self:GetNWInt("LastDamaged")
|
||||
self.frame.speed = self:GetSpeed()
|
||||
end
|
||||
|
||||
local Pos = self:GetPos()
|
||||
local Ang = self:GetAngles()
|
||||
local hight = 12
|
||||
|
||||
if self.OldWide != self:GetWide() then
|
||||
self.frame:SetSize( sizetable[self:GetWide()][1], 120 )
|
||||
self.OldWide = self:GetWide()
|
||||
end
|
||||
|
||||
if self:GetWide() == 3 then
|
||||
hight = 6
|
||||
end
|
||||
|
||||
cam.Start3D2D(Pos + Ang:Up() * 1.1 - Ang:Right() * hight + Ang:Forward() * sizetable[self:GetWide()][2], Ang, 0.1)
|
||||
self.frame:PaintManual()
|
||||
cam.End3D2D()
|
||||
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
|
||||
util.AddNetworkString("LEDDamaged")
|
||||
|
||||
function ENT:Initialize()
|
||||
|
||||
self:SetText("Made by Mac with Wire and <3")
|
||||
self:SetTColor(Vector(2.55,2,0))
|
||||
self:SetType(1)
|
||||
self:SetSpeed(1.5)
|
||||
self:SetWide(6)
|
||||
self:SetFX(1)
|
||||
self:SetOn(1)
|
||||
|
||||
self:SetModel("models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMaterial("phoenix_storms/mat/mat_phx_carbonfiber")
|
||||
self:SetColor(Color(0,0,0))
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
self:SetUseType(SIMPLE_USE)
|
||||
self:SetNWInt("LastDamaged", 0)
|
||||
local phys = self:GetPhysicsObject()
|
||||
self.nodupe = true
|
||||
self.ShareGravgun = true
|
||||
|
||||
phys:Wake()
|
||||
|
||||
|
||||
self.Inputs = WireLib.CreateSpecialInputs(self, { "Text", "Type", "Speed", "Color", "Flicker", "On" }, { "STRING", "NORMAL", "NORMAL", "VECTOR", "NORMAL", "NORMAL" })
|
||||
|
||||
end
|
||||
|
||||
function ENT:TriggerInput(iname, value)
|
||||
if iname == "Text" then
|
||||
self:SetText(value)
|
||||
elseif iname == "Type" then
|
||||
self:SetType(math.Clamp(math.Round(value), 1, 4))
|
||||
elseif iname == "Speed" then
|
||||
self:SetSpeed(math.Clamp(value, 1, 10))
|
||||
elseif iname == "Color" then
|
||||
self:SetTColor(Vector(value.x/100, value.y/100, value.z/100))
|
||||
elseif iname == "On" then
|
||||
self:SetOn(value)
|
||||
elseif iname == "Flicker" then
|
||||
self:SetFX(math.Clamp(value, 0, 1))
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
|
||||
if self:GetModel() != "models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl" then
|
||||
self:SetModel("models/squad/sf_plates/sf_plate1x"..self:GetWide()..".mdl")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage(data)
|
||||
|
||||
if data:GetDamagePosition() then
|
||||
self:EmitSound("ambient/energy/spark".. math.random(1,6) ..".wav")
|
||||
local effectdata = EffectData()
|
||||
effectdata:SetOrigin( data:GetDamagePosition() )
|
||||
util.Effect( "StunstickImpact", effectdata )
|
||||
self:SetNWInt("LastDamaged", math.Round(CurTime()+2))
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_wire_entity"
|
||||
ENT.PrintName = "Spawned Sign Wire"
|
||||
ENT.WireDebugName = "LED Screen"
|
||||
ENT.Author = "Mac"
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminSpawnable = true
|
||||
|
||||
function ENT:SetupDataTables()
|
||||
self:NetworkVar("String", 0, "Text")
|
||||
self:NetworkVar("Vector", 0, "TColor")
|
||||
self:NetworkVar("Int", 0, "Type")
|
||||
self:NetworkVar("Int", 1, "Speed")
|
||||
self:NetworkVar("Int", 2, "Wide")
|
||||
self:NetworkVar("Int", 3, "On")
|
||||
self:NetworkVar("Int", 4, "FX")
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
include( "shared.lua" )
|
||||
|
||||
function ENT:Draw()
|
||||
self.BaseClass.Draw(self)
|
||||
self.Entity:DrawModel()
|
||||
end
|
||||
@@ -0,0 +1,291 @@
|
||||
--[[
|
||||
Title: Adv. Dupe 2 Contraption Spawner
|
||||
|
||||
Desc: A mobile duplicator
|
||||
|
||||
Author: TB
|
||||
|
||||
Version: 1.0
|
||||
]]
|
||||
|
||||
AddCSLuaFile( "cl_init.lua" )
|
||||
AddCSLuaFile( "shared.lua" )
|
||||
if(WireLib)then
|
||||
include( "entities/base_wire_entity.lua" )
|
||||
end
|
||||
|
||||
include( "shared.lua" )
|
||||
|
||||
function ENT:Initialize()
|
||||
|
||||
self.Entity:SetMoveType( MOVETYPE_NONE )
|
||||
self.Entity:PhysicsInit( SOLID_VPHYSICS )
|
||||
self.Entity:SetCollisionGroup( COLLISION_GROUP_WORLD )
|
||||
self.Entity:DrawShadow( false )
|
||||
|
||||
local phys = self.Entity:GetPhysicsObject()
|
||||
if phys:IsValid() then
|
||||
phys:Wake()
|
||||
end
|
||||
|
||||
self.UndoList = {}
|
||||
self.Ghosts = {}
|
||||
|
||||
self.SpawnLastValue = 0
|
||||
self.UndoLastValue = 0
|
||||
|
||||
self.LastSpawnTime = 0
|
||||
self.DupeName = ""
|
||||
self.CurrentPropCount = 0
|
||||
|
||||
if WireLib then
|
||||
self.Inputs = Wire_CreateInputs(self.Entity, {"Spawn", "Undo"})
|
||||
self.Outputs = WireLib.CreateSpecialOutputs(self.Entity, {"Out"}, { "NORMAL" })
|
||||
end
|
||||
end
|
||||
|
||||
/*-----------------------------------------------------------------------*
|
||||
* Sets options for this spawner
|
||||
*-----------------------------------------------------------------------*/
|
||||
function ENT:SetOptions(ply, delay, undo_delay, key, undo_key, disgrav, disdrag, addvel, hideprops )
|
||||
|
||||
self.delay = delay
|
||||
self.undo_delay = undo_delay
|
||||
|
||||
--Key bindings
|
||||
self.key = key
|
||||
self.undo_key = undo_key
|
||||
numpad.Remove( self.CreateKey )
|
||||
numpad.Remove( self.UndoKey )
|
||||
self.CreateKey = numpad.OnDown( ply, self.key , "ContrSpawnerCreate", self.Entity, true )
|
||||
self.UndoKey = numpad.OnDown( ply, self.undo_key, "ContrSpawnerUndo" , self.Entity, true )
|
||||
|
||||
-- Other parameters
|
||||
self.DisableGravity = disgrav
|
||||
self.DisableDrag = disdrag
|
||||
self.AddVelocity = addvel
|
||||
self.HideProps = hideprops
|
||||
|
||||
-- Store the player's current dupe name
|
||||
self.DupeName = tostring(ply.AdvDupe2.Name)
|
||||
|
||||
self:ShowOutput()
|
||||
end
|
||||
|
||||
function ENT:UpdateOptions( options )
|
||||
self:SetOptions( options["delay"], options["undo_delay"], options["key"], options["undo_key"])
|
||||
end
|
||||
|
||||
function ENT:AddGhosts()
|
||||
if self.HideProps then return end
|
||||
local moveable = self:GetPhysicsObject():IsMoveable()
|
||||
self:GetPhysicsObject():EnableMotion(false)
|
||||
local EntTable, GhostEntity, Phys
|
||||
local Offset = self.DupeAngle - self.EntAngle
|
||||
for EntIndex,v in pairs(self.EntityTable)do
|
||||
if(EntIndex!=self.HeadEnt)then
|
||||
if(self.EntityTable[EntIndex].Class=="gmod_contr_spawner")then self.EntityTable[EntIndex] = nil continue end
|
||||
EntTable = table.Copy(self.EntityTable[EntIndex])
|
||||
if(EntTable.BuildDupeInfo && EntTable.BuildDupeInfo.PhysicsObjects)then
|
||||
Phys = EntTable.BuildDupeInfo.PhysicsObjects[0]
|
||||
else
|
||||
if(!v.BuildDupeInfo)then v.BuildDupeInfo = {} end
|
||||
v.BuildDupeInfo.PhysicsObjects = table.Copy(v.PhysicsObjects)
|
||||
Phys = EntTable.PhysicsObjects[0]
|
||||
end
|
||||
|
||||
GhostEntity = nil
|
||||
|
||||
if(EntTable.Model==nil || !util.IsValidModel(EntTable.Model)) then EntTable.Model="models/error.mdl" end
|
||||
|
||||
if ( EntTable.Model:sub( 1, 1 ) == "*" ) then
|
||||
GhostEntity = ents.Create( "func_physbox" )
|
||||
else
|
||||
GhostEntity = ents.Create( "gmod_ghost" )
|
||||
end
|
||||
|
||||
// If there are too many entities we might not spawn..
|
||||
if ( !GhostEntity || GhostEntity == NULL ) then return end
|
||||
|
||||
duplicator.DoGeneric( GhostEntity, EntTable )
|
||||
|
||||
GhostEntity:Spawn()
|
||||
|
||||
GhostEntity:DrawShadow( false )
|
||||
GhostEntity:SetMoveType( MOVETYPE_NONE )
|
||||
GhostEntity:SetSolid( SOLID_VPHYSICS );
|
||||
GhostEntity:SetNotSolid( true )
|
||||
GhostEntity:SetRenderMode( RENDERMODE_TRANSALPHA )
|
||||
GhostEntity:SetColor( Color(255, 255, 255, 150) )
|
||||
|
||||
GhostEntity:SetAngles(Phys.Angle)
|
||||
GhostEntity:SetPos(self:GetPos() + Phys.Pos - self.Offset)
|
||||
self:SetAngles(self.EntAngle)
|
||||
GhostEntity:SetParent( self )
|
||||
self:SetAngles(self.DupeAngle)
|
||||
self.Ghosts[EntIndex] = GhostEntity
|
||||
end
|
||||
end
|
||||
self:SetAngles(self.DupeAngle)
|
||||
self:GetPhysicsObject():EnableMotion(moveable)
|
||||
end
|
||||
|
||||
function ENT:GetCreationDelay() return self.delay end
|
||||
function ENT:GetDeletionDelay() return self.undo_delay end
|
||||
|
||||
function ENT:OnTakeDamage( dmginfo ) self.Entity:TakePhysicsDamage( dmginfo ) end
|
||||
|
||||
function ENT:SetDupeInfo( HeadEnt, EntityTable, ConstraintTable )
|
||||
self.HeadEnt = HeadEnt
|
||||
self.EntityTable = EntityTable
|
||||
self.ConstraintTable = ConstraintTable
|
||||
if(!self.DupeAngle)then self.DupeAngle = self:GetAngles() end
|
||||
if(!self.EntAngle)then self.EntAngle = EntityTable[HeadEnt].PhysicsObjects[0].Angle end
|
||||
if(!self.Offset)then self.Offset = self.EntityTable[HeadEnt].PhysicsObjects[0].Pos end
|
||||
|
||||
local headpos, headang = EntityTable[HeadEnt].PhysicsObjects[0].Pos, EntityTable[HeadEnt].PhysicsObjects[0].Angle
|
||||
for k, v in pairs(EntityTable) do
|
||||
for o, p in pairs(v.PhysicsObjects) do
|
||||
p.LPos, p.LAngle = WorldToLocal(p.Pos, p.Angle, headpos, headang)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:DoSpawn( ply )
|
||||
-- Explicitly allow spawning if no player is provided, but an invalid player gets denied. This can happen when a player leaves the server.
|
||||
if not (ply and ply:IsValid()) then return end
|
||||
|
||||
for k, v in pairs(self.EntityTable) do
|
||||
for o, p in pairs(v.PhysicsObjects) do
|
||||
p.Pos, p.Angle = self:LocalToWorld(p.LPos), self:LocalToWorldAngles(p.LAngle)
|
||||
end
|
||||
end
|
||||
|
||||
/*local AngleOffset = self.EntAngle
|
||||
AngleOffset = self:GetAngles() - AngleOffset
|
||||
local AngleOffset2 = Angle(0,0,0)
|
||||
//AngleOffset2.y = AngleOffset.y
|
||||
AngleOffset2:RotateAroundAxis(self:GetUp(), AngleOffset.y)
|
||||
AngleOffset2:RotateAroundAxis(self:GetRight(),AngleOffset.p)
|
||||
AngleOffset2:RotateAroundAxis(self:GetForward(),AngleOffset.r)*/
|
||||
|
||||
local Ents, Constrs = AdvDupe2.duplicator.Paste(ply, self.EntityTable, self.ConstraintTable, nil, nil, Vector(0,0,0), true)
|
||||
local i = #self.UndoList+1
|
||||
self.UndoList[i] = Ents
|
||||
|
||||
local undotxt = "AdvDupe2: Contraption ("..tostring(self.DupeName)..")"
|
||||
|
||||
undo.Create(undotxt)
|
||||
local phys
|
||||
for k,ent in pairs(Ents)do
|
||||
phys = ent:GetPhysicsObject()
|
||||
if IsValid(phys) then
|
||||
phys:Wake()
|
||||
if(self.DisableGravity==1)then phys:EnableGravity(false) end
|
||||
if(self.DisableDrag==1)then phys:EnableDrag(false) end
|
||||
phys:EnableMotion(true)
|
||||
if(ent.SetForce)then ent.SetForce(ent, ent.force, ent.mul) end
|
||||
if(self.AddVelocity==1)then
|
||||
phys:SetVelocity( self:GetVelocity() )
|
||||
phys:AddAngleVelocity( self:GetPhysicsObject():GetAngleVelocity() )
|
||||
end
|
||||
end
|
||||
|
||||
undo.AddEntity(ent)
|
||||
end
|
||||
|
||||
undo.SetPlayer(ply)
|
||||
undo.Finish()
|
||||
|
||||
if(self.undo_delay>0)then
|
||||
timer.Simple(self.undo_delay, function()
|
||||
if(self.UndoList && self.UndoList[i])then
|
||||
for k,ent in pairs(self.UndoList[i]) do
|
||||
if(IsValid(ent)) then
|
||||
ent:Remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:DoUndo( ply )
|
||||
|
||||
if(!self.UndoList || #self.UndoList == 0)then return end
|
||||
|
||||
local entities = self.UndoList[ #self.UndoList ]
|
||||
self.UndoList[ #self.UndoList ] = nil
|
||||
for _,ent in pairs(entities) do
|
||||
if (IsValid(ent)) then
|
||||
ent:Remove()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:TriggerInput(iname, value)
|
||||
local ply = self:GetPlayer()
|
||||
|
||||
if(iname == "Spawn")then
|
||||
if ((value > 0) == self.SpawnLastValue) then return end
|
||||
self.SpawnLastValue = (value > 0)
|
||||
|
||||
if(self.SpawnLastValue)then
|
||||
local delay = self:GetCreationDelay()
|
||||
if (delay == 0) then self:DoSpawn( ply ) return end
|
||||
if(CurTime() < self.LastSpawnTime)then return end
|
||||
self:DoSpawn( ply )
|
||||
self.LastSpawnTime=CurTime()+delay
|
||||
end
|
||||
elseif (iname == "Undo") then
|
||||
// Same here
|
||||
if((value > 0) == self.UndoLastValue)then return end
|
||||
self.UndoLastValue = (value > 0)
|
||||
|
||||
if(self.UndoLastValue)then self:DoUndo(ply) end
|
||||
end
|
||||
end
|
||||
|
||||
local flags = {"Enabled", "Disabled"}
|
||||
function ENT:ShowOutput()
|
||||
local text = "\nGravity: "..((self.DisableGravity == 1) and flags[1] or flags[2])
|
||||
text = text.."\nDrag: " ..((self.DisableDrag == 1) and flags[1] or flags[2])
|
||||
text = text.."\nVelocity: "..((self.AddVelocity == 1) and flags[1] or flags[2])
|
||||
|
||||
self.Entity:SetOverlayText(
|
||||
"Spawn Name: " .. tostring(self.DupeName) ..
|
||||
"\nSpawn Delay: " .. tostring(self:GetCreationDelay()) ..
|
||||
"\nUndo Delay: ".. tostring(self:GetDeletionDelay()) ..
|
||||
text
|
||||
)
|
||||
end
|
||||
|
||||
/*-----------------------------------------------------------------------*
|
||||
* Handler for spawn keypad input
|
||||
*-----------------------------------------------------------------------*/
|
||||
function SpawnContrSpawner( ply, ent )
|
||||
|
||||
if (!ent || !ent:IsValid()) then return end
|
||||
|
||||
local delay = ent:GetTable():GetCreationDelay()
|
||||
|
||||
if(delay == 0) then
|
||||
ent:DoSpawn( ply )
|
||||
return
|
||||
end
|
||||
|
||||
if(CurTime() < ent.LastSpawnTime)then return end
|
||||
ent:DoSpawn( ply )
|
||||
ent.LastSpawnTime=CurTime()+delay
|
||||
end
|
||||
|
||||
/*-----------------------------------------------------------------------*
|
||||
* Handler for undo keypad input
|
||||
*-----------------------------------------------------------------------*/
|
||||
function UndoContrSpawner( ply, ent )
|
||||
if (!ent || !ent:IsValid()) then return end
|
||||
ent:DoUndo( ply, true )
|
||||
end
|
||||
|
||||
numpad.Register( "ContrSpawnerCreate", SpawnContrSpawner )
|
||||
numpad.Register( "ContrSpawnerUndo" , UndoContrSpawner )
|
||||
@@ -0,0 +1,10 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = WireLib and "base_wire_entity" or "base_gmodentity"
|
||||
ENT.PrintName = "Contraption Spawner"
|
||||
ENT.Author = "TB"
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminSpawnable = false
|
||||
@@ -0,0 +1,141 @@
|
||||
AddCSLuaFile()
|
||||
|
||||
ENT.Type = "anim"
|
||||
|
||||
ENT.PrintName = ""
|
||||
ENT.Author = ""
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminOnly = false
|
||||
|
||||
--[[---------------------------------------------------------
|
||||
Name: Initialize
|
||||
-----------------------------------------------------------]]
|
||||
function ENT:Initialize()
|
||||
|
||||
local Radius = 6
|
||||
local min = Vector( 1, 1, 1 ) * Radius * -0.5
|
||||
local max = Vector( 1, 1, 1 ) * Radius * 0.5
|
||||
|
||||
if ( SERVER ) then
|
||||
|
||||
self.AttachedEntity = ents.Create( "prop_dynamic" )
|
||||
self.AttachedEntity:SetModel( self:GetModel() )
|
||||
self.AttachedEntity:SetAngles( self:GetAngles() )
|
||||
self.AttachedEntity:SetPos( self:GetPos() )
|
||||
self.AttachedEntity:SetSkin( self:GetSkin() )
|
||||
self.AttachedEntity:Spawn()
|
||||
self.AttachedEntity:SetParent( self.Entity )
|
||||
self.AttachedEntity:DrawShadow( false )
|
||||
|
||||
self:SetModel( "models/props_junk/watermelon01.mdl" )
|
||||
|
||||
self:DeleteOnRemove( self.AttachedEntity )
|
||||
|
||||
-- Don't use the model's physics - create a box instead
|
||||
self:PhysicsInitBox( min, max )
|
||||
|
||||
-- Set up our physics object here
|
||||
local phys = self:GetPhysicsObject()
|
||||
if ( IsValid( phys ) ) then
|
||||
phys:Wake()
|
||||
phys:EnableGravity( false )
|
||||
phys:EnableDrag( false )
|
||||
end
|
||||
|
||||
self:DrawShadow( false )
|
||||
self:SetCollisionGroup( COLLISION_GROUP_WEAPON )
|
||||
|
||||
else
|
||||
|
||||
self.GripMaterial = Material( "sprites/grip" )
|
||||
|
||||
-- Get the attached entity so that clientside functions like properties can interact with it
|
||||
local tab = ents.FindByClassAndParent( "prop_dynamic", self )
|
||||
if ( tab && IsValid( tab[ 1 ] ) ) then self.AttachedEntity = tab[ 1 ] end
|
||||
|
||||
end
|
||||
|
||||
-- Set collision bounds exactly
|
||||
self:SetCollisionBounds( min, max )
|
||||
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------
|
||||
Name: Draw
|
||||
-----------------------------------------------------------]]
|
||||
function ENT:Draw()
|
||||
|
||||
render.SetMaterial( self.GripMaterial )
|
||||
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------
|
||||
Name: PhysicsUpdate
|
||||
-----------------------------------------------------------]]
|
||||
function ENT:PhysicsUpdate( physobj )
|
||||
|
||||
if ( CLIENT ) then return end
|
||||
|
||||
-- Don't do anything if the player isn't holding us
|
||||
if ( !self:IsPlayerHolding() && !self:IsConstrained() ) then
|
||||
|
||||
physobj:SetVelocity( Vector( 0, 0, 0 ) )
|
||||
physobj:Sleep()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------
|
||||
Name: Called after entity 'copy'
|
||||
-----------------------------------------------------------]]
|
||||
function ENT:OnEntityCopyTableFinish( tab )
|
||||
|
||||
-- We need to store the model of the attached entity
|
||||
-- Not the one we have here.
|
||||
tab.Model = self.AttachedEntity:GetModel()
|
||||
|
||||
-- Store the attached entity's table so we can restore it after being pasted
|
||||
tab.AttachedEntityInfo = table.Copy( duplicator.CopyEntTable( self.AttachedEntity ) )
|
||||
tab.AttachedEntityInfo.Pos = nil -- Don't even save angles and position, we are a parented entity
|
||||
tab.AttachedEntityInfo.Angle = nil
|
||||
|
||||
-- Do NOT store the attached entity itself in our table!
|
||||
-- Otherwise, if we copy-paste the prop with the duplicator, its AttachedEntity value will point towards the original prop's attached entity instead, and that'll break stuff
|
||||
tab.AttachedEntity = nil
|
||||
|
||||
end
|
||||
|
||||
|
||||
--[[---------------------------------------------------------
|
||||
Name: PostEntityPaste
|
||||
-----------------------------------------------------------]]
|
||||
function ENT:PostEntityPaste( ply )
|
||||
|
||||
-- Restore the attached entity using the information we've saved
|
||||
if ( IsValid( self.AttachedEntity ) ) and ( self.AttachedEntityInfo ) then
|
||||
|
||||
-- Apply skin, bodygroups, bone manipulator, etc.
|
||||
duplicator.DoGeneric( self.AttachedEntity, self.AttachedEntityInfo )
|
||||
|
||||
if ( self.AttachedEntityInfo.EntityMods ) then
|
||||
self.AttachedEntity.EntityMods = table.Copy( self.AttachedEntityInfo.EntityMods )
|
||||
duplicator.ApplyEntityModifiers( ply, self.AttachedEntity )
|
||||
end
|
||||
|
||||
if ( self.AttachedEntityInfo.BoneMods ) then
|
||||
self.AttachedEntity.BoneMods = table.Copy( self.AttachedEntityInfo.BoneMods )
|
||||
duplicator.ApplyBoneModifiers( ply, self.AttachedEntity )
|
||||
end
|
||||
|
||||
self.AttachedEntityInfo = nil
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
386
garrysmod/gamemodes/militaryrp/plugins/buildtools/netstream.lua
Normal file
386
garrysmod/gamemodes/militaryrp/plugins/buildtools/netstream.lua
Normal file
@@ -0,0 +1,386 @@
|
||||
--A net extension which allows sending large streams of data without overflowing the reliable channel
|
||||
--Keep it in lua/autorun so it will be shared between addons
|
||||
AddCSLuaFile()
|
||||
|
||||
net.Stream = {}
|
||||
net.Stream.SendSize = 20000 --This is the size of each packet to send
|
||||
net.Stream.Timeout = 30 --How long to wait for client response before cleaning up
|
||||
net.Stream.MaxWriteStreams = 1024 --The maximum number of write data items to store
|
||||
net.Stream.MaxReadStreams = 128 --The maximum number of queued read data items to store
|
||||
net.Stream.MaxChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB
|
||||
net.Stream.MaxSize = net.Stream.SendSize*net.Stream.MaxChunks
|
||||
net.Stream.MaxTries = 3 --Maximum times the client may retry downloading the whole data
|
||||
|
||||
local WriteStreamQueue = {
|
||||
__index = {
|
||||
Add = function(self, stream)
|
||||
local identifier = self.curidentifier
|
||||
local startid = identifier
|
||||
while self.queue[identifier] do
|
||||
identifier = identifier % net.Stream.MaxWriteStreams + 1
|
||||
if identifier == startid then
|
||||
ErrorNoHalt("Netstream is full of WriteStreams!")
|
||||
net.WriteUInt(0, 32)
|
||||
return
|
||||
end
|
||||
end
|
||||
self.curidentifier = identifier % net.Stream.MaxWriteStreams + 1
|
||||
|
||||
if next(self.queue)==nil then
|
||||
self.activitytimeout = CurTime()+net.Stream.Timeout
|
||||
timer.Create("netstream_queueclean", 5, 0, function() self:Clean() end)
|
||||
end
|
||||
self.queue[identifier] = stream
|
||||
stream.identifier = identifier
|
||||
return stream
|
||||
end,
|
||||
|
||||
Write = function(self, ply)
|
||||
local identifier = net.ReadUInt(32)
|
||||
local chunkidx = net.ReadUInt(32)
|
||||
local stream = self.queue[identifier]
|
||||
--print("Got request", identifier, chunkidx, stream)
|
||||
if stream then
|
||||
if stream:Write(ply, chunkidx) then
|
||||
self.activitytimeout = CurTime()+net.Stream.Timeout
|
||||
stream.timeout = CurTime()+net.Stream.Timeout
|
||||
end
|
||||
else
|
||||
-- Tell them the stream doesn't exist
|
||||
net.Start("NetStreamRead")
|
||||
net.WriteUInt(identifier, 32)
|
||||
net.WriteUInt(0, 32)
|
||||
if SERVER then net.Send(ply) else net.SendToServer() end
|
||||
end
|
||||
end,
|
||||
|
||||
Clean = function(self)
|
||||
local t = CurTime()
|
||||
for k, stream in pairs(self.queue) do
|
||||
if (next(stream.clients)~=nil and t >= stream.timeout) or t >= self.activitytimeout then
|
||||
stream:Remove()
|
||||
self.queue[k] = nil
|
||||
end
|
||||
end
|
||||
if next(self.queue)==nil then
|
||||
timer.Remove("netstream_queueclean")
|
||||
end
|
||||
end,
|
||||
},
|
||||
__call = function(t)
|
||||
return setmetatable({
|
||||
activitytimeout = CurTime()+net.Stream.Timeout,
|
||||
curidentifier = 1,
|
||||
queue = {}
|
||||
}, t)
|
||||
end
|
||||
}
|
||||
setmetatable(WriteStreamQueue, WriteStreamQueue)
|
||||
net.Stream.WriteStreams = WriteStreamQueue()
|
||||
|
||||
local ReadStreamQueue = {
|
||||
__index = {
|
||||
Add = function(self, stream)
|
||||
local queue = self.queues[stream.player]
|
||||
|
||||
if #queue == net.Stream.MaxReadStreams then
|
||||
ErrorNoHalt("Receiving too many ReadStream requests!")
|
||||
return
|
||||
end
|
||||
|
||||
for _, v in ipairs(queue) do
|
||||
if v.identifier == stream.identifier then
|
||||
ErrorNoHalt("Tried to start a new ReadStream for an already existing stream!")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
queue[#queue+1] = stream
|
||||
if #queue == 1 then
|
||||
stream:Request()
|
||||
end
|
||||
return stream
|
||||
end,
|
||||
|
||||
Remove = function(self, stream)
|
||||
local queue = rawget(self.queues, stream.player)
|
||||
if queue then
|
||||
if stream == queue[1] then
|
||||
table.remove(queue, 1)
|
||||
local nextInQueue = queue[1]
|
||||
if nextInQueue then
|
||||
nextInQueue:Request()
|
||||
else
|
||||
self.queues[stream.player] = nil
|
||||
end
|
||||
else
|
||||
for k, v in ipairs(queue) do
|
||||
if v == stream then
|
||||
table.remove(queue, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
Read = function(self, ply)
|
||||
local identifier = net.ReadUInt(32)
|
||||
local queue = rawget(self.queues, ply)
|
||||
if queue and queue[1] then
|
||||
queue[1]:Read(identifier)
|
||||
end
|
||||
end
|
||||
},
|
||||
__call = function(t)
|
||||
return setmetatable({
|
||||
queues = setmetatable({}, {__index = function(t,k) local r={} t[k]=r return r end})
|
||||
}, t)
|
||||
end
|
||||
}
|
||||
setmetatable(ReadStreamQueue, ReadStreamQueue)
|
||||
net.Stream.ReadStreams = ReadStreamQueue()
|
||||
|
||||
|
||||
local WritingDataItem = {
|
||||
__index = {
|
||||
Write = function(self, ply, chunkidx)
|
||||
local client = self.clients[ply]
|
||||
if client.finished then return false end
|
||||
if chunkidx == #self.chunks+1 then self:Finished(ply) return true end
|
||||
|
||||
if client.downloads+#self.chunks-client.progress >= net.Stream.MaxTries * #self.chunks then self:Finished(ply) return false end
|
||||
client.downloads = client.downloads + 1
|
||||
|
||||
local chunk = self.chunks[chunkidx]
|
||||
if not chunk then return false end
|
||||
|
||||
client.progress = chunkidx
|
||||
|
||||
--print("Sending", "NetStreamRead", self.identifier, #chunk.data, chunkidx, chunk.crc)
|
||||
net.Start("NetStreamRead")
|
||||
net.WriteUInt(self.identifier, 32)
|
||||
net.WriteUInt(#chunk.data, 32)
|
||||
net.WriteUInt(chunkidx, 32)
|
||||
net.WriteString(chunk.crc)
|
||||
net.WriteData(chunk.data, #chunk.data)
|
||||
if CLIENT then net.SendToServer() else net.Send(ply) end
|
||||
return true
|
||||
end,
|
||||
|
||||
Finished = function(self, ply)
|
||||
self.clients[ply].finished = true
|
||||
if self.callback then
|
||||
local ok, err = xpcall(self.callback, debug.traceback, ply)
|
||||
if not ok then ErrorNoHalt(err) end
|
||||
end
|
||||
end,
|
||||
|
||||
GetProgress = function(self, ply)
|
||||
return self.clients[ply].progress / #self.chunks
|
||||
end,
|
||||
|
||||
Remove = function(self)
|
||||
local sendTo = {}
|
||||
for ply, client in pairs(self.clients) do
|
||||
if not client.finished then
|
||||
client.finished = true
|
||||
if CLIENT or ply:IsValid() then sendTo[#sendTo+1] = ply end
|
||||
end
|
||||
end
|
||||
|
||||
if next(sendTo)~=nil then
|
||||
--print("Sending", "NetStreamRead", self.identifier, 0)
|
||||
net.Start("NetStreamRead")
|
||||
net.WriteUInt(self.identifier, 32)
|
||||
net.WriteUInt(0, 32)
|
||||
if SERVER then net.Send(sendTo) else net.SendToServer() end
|
||||
end
|
||||
end
|
||||
|
||||
},
|
||||
__call = function(t, data, callback)
|
||||
local chunks = {}
|
||||
for i=1, math.ceil(#data / net.Stream.SendSize) do
|
||||
local datachunk = string.sub(data, (i - 1) * net.Stream.SendSize + 1, i * net.Stream.SendSize)
|
||||
chunks[i] = { data = datachunk, crc = util.CRC(datachunk) }
|
||||
end
|
||||
|
||||
return setmetatable({
|
||||
timeout = CurTime()+net.Stream.Timeout,
|
||||
chunks = chunks,
|
||||
callback = callback,
|
||||
lasttouched = 0,
|
||||
clients = setmetatable({},{__index = function(t,k)
|
||||
local r = {
|
||||
finished = false,
|
||||
downloads = 0,
|
||||
progress = 0,
|
||||
} t[k]=r return r
|
||||
end})
|
||||
}, t)
|
||||
end
|
||||
}
|
||||
setmetatable(WritingDataItem, WritingDataItem)
|
||||
|
||||
local ReadingDataItem = {
|
||||
__index = {
|
||||
Request = function(self)
|
||||
if self.downloads+self.numchunks-#self.chunks >= net.Stream.MaxTries*self.numchunks then self:Remove() return end
|
||||
self.downloads = self.downloads + 1
|
||||
timer.Create("NetStreamReadTimeout" .. self.identifier, net.Stream.Timeout*0.5, 1, function() self:Request() end)
|
||||
self:WriteRequest()
|
||||
end,
|
||||
|
||||
WriteRequest = function(self)
|
||||
--print("Requesting", self.identifier, #self.chunks)
|
||||
net.Start("NetStreamWrite")
|
||||
net.WriteUInt(self.identifier, 32)
|
||||
net.WriteUInt(#self.chunks+1, 32)
|
||||
if CLIENT then net.SendToServer() else net.Send(self.player) end
|
||||
end,
|
||||
|
||||
Read = function(self, identifier)
|
||||
if self.identifier ~= identifier then self:Request() return end
|
||||
|
||||
local size = net.ReadUInt(32)
|
||||
if size == 0 then self:Remove() return end
|
||||
|
||||
local chunkidx = net.ReadUInt(32)
|
||||
if chunkidx ~= #self.chunks+1 then self:Request() return end
|
||||
|
||||
local crc = net.ReadString()
|
||||
local data = net.ReadData(size)
|
||||
|
||||
if crc ~= util.CRC(data) then self:Request() return end
|
||||
|
||||
self.chunks[chunkidx] = data
|
||||
if #self.chunks == self.numchunks then self:Remove(true) return end
|
||||
|
||||
self:Request()
|
||||
end,
|
||||
|
||||
GetProgress = function(self)
|
||||
return #self.chunks/self.numchunks
|
||||
end,
|
||||
|
||||
Remove = function(self, finished)
|
||||
timer.Remove("NetStreamReadTimeout" .. self.identifier)
|
||||
|
||||
local data
|
||||
if finished then
|
||||
data = table.concat(self.chunks)
|
||||
if self.compressed then
|
||||
data = util.Decompress(data, net.Stream.MaxSize)
|
||||
end
|
||||
self:WriteRequest() -- Notify we finished
|
||||
end
|
||||
|
||||
local ok, err = xpcall(self.callback, debug.traceback, data)
|
||||
if not ok then ErrorNoHalt(err) end
|
||||
|
||||
net.Stream.ReadStreams:Remove(self)
|
||||
end
|
||||
},
|
||||
__call = function(t, ply, callback, numchunks, identifier, compressed)
|
||||
return setmetatable({
|
||||
identifier = identifier,
|
||||
chunks = {},
|
||||
compressed = compressed,
|
||||
numchunks = numchunks,
|
||||
callback = callback,
|
||||
player = ply,
|
||||
downloads = 0
|
||||
}, t)
|
||||
end
|
||||
}
|
||||
setmetatable(ReadingDataItem, ReadingDataItem)
|
||||
|
||||
|
||||
function net.WriteStream(data, callback, dontcompress)
|
||||
if not isstring(data) then
|
||||
error("bad argument #1 to 'WriteStream' (string expected, got " .. type(data) .. ")", 2)
|
||||
end
|
||||
if callback ~= nil and not isfunction(callback) then
|
||||
error("bad argument #2 to 'WriteStream' (function expected, got " .. type(callback) .. ")", 2)
|
||||
end
|
||||
|
||||
local compressed = not dontcompress
|
||||
if compressed then
|
||||
data = util.Compress(data) or ""
|
||||
end
|
||||
|
||||
if #data == 0 then
|
||||
net.WriteUInt(0, 32)
|
||||
return
|
||||
end
|
||||
|
||||
if #data > net.Stream.MaxSize then
|
||||
ErrorNoHalt("net.WriteStream request is too large! ", #data/1048576, "MiB")
|
||||
net.WriteUInt(0, 32)
|
||||
return
|
||||
end
|
||||
|
||||
local stream = net.Stream.WriteStreams:Add(WritingDataItem(data, callback, compressed))
|
||||
if not stream then return end
|
||||
|
||||
--print("WriteStream", #stream.chunks, stream.identifier, compressed)
|
||||
net.WriteUInt(#stream.chunks, 32)
|
||||
net.WriteUInt(stream.identifier, 32)
|
||||
net.WriteBool(compressed)
|
||||
|
||||
return stream
|
||||
end
|
||||
|
||||
--If the receiver is a player then add it to a queue.
|
||||
--If the receiver is the server then add it to a queue for each individual player
|
||||
function net.ReadStream(ply, callback)
|
||||
if CLIENT then
|
||||
ply = NULL
|
||||
else
|
||||
if type(ply) ~= "Player" then
|
||||
error("bad argument #1 to 'ReadStream' (Player expected, got " .. type(ply) .. ")", 2)
|
||||
elseif not ply:IsValid() then
|
||||
error("bad argument #1 to 'ReadStream' (Tried to use a NULL entity!)", 2)
|
||||
end
|
||||
end
|
||||
if not isfunction(callback) then
|
||||
error("bad argument #2 to 'ReadStream' (function expected, got " .. type(callback) .. ")", 2)
|
||||
end
|
||||
|
||||
local numchunks = net.ReadUInt(32)
|
||||
if numchunks == nil then
|
||||
return
|
||||
elseif numchunks == 0 then
|
||||
local ok, err = xpcall(callback, debug.traceback, "")
|
||||
if not ok then ErrorNoHalt(err) end
|
||||
return
|
||||
end
|
||||
|
||||
local identifier = net.ReadUInt(32)
|
||||
local compressed = net.ReadBool()
|
||||
|
||||
if numchunks > net.Stream.MaxChunks then
|
||||
ErrorNoHalt("ReadStream requests from ", ply, " is too large! ", numchunks * net.Stream.SendSize / 1048576, "MiB")
|
||||
return
|
||||
end
|
||||
|
||||
--print("ReadStream", numchunks, identifier, compressed)
|
||||
|
||||
return net.Stream.ReadStreams:Add(ReadingDataItem(ply, callback, numchunks, identifier, compressed))
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
util.AddNetworkString("NetStreamWrite")
|
||||
util.AddNetworkString("NetStreamRead")
|
||||
end
|
||||
|
||||
--Send requested stream data
|
||||
net.Receive("NetStreamWrite", function(len, ply)
|
||||
net.Stream.WriteStreams:Write(ply or NULL)
|
||||
end)
|
||||
|
||||
--Download the sent stream data
|
||||
net.Receive("NetStreamRead", function(len, ply)
|
||||
net.Stream.ReadStreams:Read(ply or NULL)
|
||||
end)
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
local function PermaPropsViewer()
|
||||
|
||||
if not LocalPlayer().DrawPPEnt or not istable(LocalPlayer().DrawPPEnt) then return end
|
||||
|
||||
local pos = LocalPlayer():EyePos() + LocalPlayer():EyeAngles():Forward() * 10
|
||||
local ang = LocalPlayer():EyeAngles()
|
||||
|
||||
ang = Angle(ang.p + 90, ang.y, 0)
|
||||
|
||||
for k, v in pairs(LocalPlayer().DrawPPEnt) do
|
||||
|
||||
if not v or not v:IsValid() then LocalPlayer().DrawPPEnt[k] = nil continue end
|
||||
|
||||
render.ClearStencil()
|
||||
render.SetStencilEnable(true)
|
||||
render.SetStencilWriteMask(255)
|
||||
render.SetStencilTestMask(255)
|
||||
render.SetStencilReferenceValue(15)
|
||||
render.SetStencilFailOperation(STENCILOPERATION_REPLACE)
|
||||
render.SetStencilZFailOperation(STENCILOPERATION_REPLACE)
|
||||
render.SetStencilPassOperation(STENCILOPERATION_KEEP)
|
||||
render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_ALWAYS)
|
||||
render.SetBlend(0)
|
||||
v:DrawModel()
|
||||
render.SetBlend(1)
|
||||
render.SetStencilCompareFunction(STENCILCOMPARISONFUNCTION_EQUAL)
|
||||
cam.Start3D2D(pos, ang, 1)
|
||||
surface.SetDrawColor(255, 0, 0, 255)
|
||||
surface.DrawRect(-ScrW(), -ScrH(), ScrW() * 2, ScrH() * 2)
|
||||
cam.End3D2D()
|
||||
v:DrawModel()
|
||||
render.SetStencilEnable(false)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
hook.Add("PostDrawOpaqueRenderables", "PermaPropsViewer", PermaPropsViewer)
|
||||
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
surface.CreateFont( "pp_font", {
|
||||
font = "Arial",
|
||||
size = 20,
|
||||
weight = 700,
|
||||
shadow = false
|
||||
} )
|
||||
|
||||
local function pp_open_menu()
|
||||
|
||||
local Len = net.ReadFloat()
|
||||
local Data = net.ReadData( Len )
|
||||
local UnCompress = util.Decompress( Data )
|
||||
local Content = util.JSONToTable( UnCompress )
|
||||
|
||||
local Main = vgui.Create( "DFrame" )
|
||||
Main:SetSize( 600, 355 )
|
||||
Main:Center()
|
||||
Main:SetTitle("")
|
||||
Main:SetVisible( true )
|
||||
Main:SetDraggable( true )
|
||||
Main:ShowCloseButton( true )
|
||||
Main:MakePopup()
|
||||
Main.Paint = function(self)
|
||||
|
||||
draw.RoundedBox( 0, 0, 0, self:GetWide(), self:GetTall(), Color(155, 155, 155, 220) )
|
||||
surface.SetDrawColor( 17, 148, 240, 255 )
|
||||
surface.DrawOutlinedRect( 0, 0, self:GetWide(), self:GetTall() )
|
||||
|
||||
draw.RoundedBox( 0, 0, 0, self:GetWide(), 25, Color(17, 148, 240, 200) )
|
||||
surface.SetDrawColor( 17, 148, 240, 255 )
|
||||
surface.DrawOutlinedRect( 0, 0, self:GetWide(), 25 )
|
||||
draw.DrawText( "PermaProps Config", "pp_font", 10, 2.2, Color(255, 255, 255, 255), TEXT_ALIGN_LEFT )
|
||||
|
||||
end
|
||||
|
||||
local BSelect
|
||||
local PSelect
|
||||
|
||||
local MainPanel = vgui.Create( "DPanel", Main )
|
||||
MainPanel:SetPos( 190, 51 )
|
||||
MainPanel:SetSize( 390, 275 )
|
||||
MainPanel.Paint = function( self )
|
||||
surface.SetDrawColor( 50, 50, 50, 200 )
|
||||
surface.DrawRect( 0, 0, self:GetWide(), self:GetTall() )
|
||||
surface.DrawOutlinedRect(0, 15, self:GetWide(), 40)
|
||||
end
|
||||
PSelect = MainPanel
|
||||
|
||||
local MainLabel = vgui.Create("DLabel", MainPanel)
|
||||
MainLabel:SetFont("pp_font")
|
||||
MainLabel:SetPos(140, 25)
|
||||
MainLabel:SetColor(Color(50, 50, 50, 255))
|
||||
MainLabel:SetText("Hey ".. LocalPlayer():Nick() .." !")
|
||||
MainLabel:SizeToContents()
|
||||
|
||||
local MainLabel2 = vgui.Create("DLabel", MainPanel)
|
||||
MainLabel2:SetFont("pp_font")
|
||||
MainLabel2:SetPos(80, 80)
|
||||
MainLabel2:SetColor(Color(50, 50, 50, 255))
|
||||
MainLabel2:SetText("There are ".. ( Content.MProps or 0 ) .." props on this map.\n\nThere are ".. ( Content.TProps or 0 ) .." props in the DB.")
|
||||
MainLabel2:SizeToContents()
|
||||
|
||||
local RemoveMapProps = vgui.Create( "DButton", MainPanel )
|
||||
RemoveMapProps:SetText( " Clear map props " )
|
||||
RemoveMapProps:SetFont("pp_font")
|
||||
RemoveMapProps:SetSize( 370, 30)
|
||||
RemoveMapProps:SetPos( 10, 160 )
|
||||
RemoveMapProps:SetTextColor( Color( 50, 50, 50, 255 ) )
|
||||
RemoveMapProps.DoClick = function()
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "CLR_MAP"})
|
||||
net.SendToServer()
|
||||
end
|
||||
RemoveMapProps.Paint = function(self)
|
||||
surface.SetDrawColor(50, 50, 50, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
end
|
||||
|
||||
local ClearMapProps = vgui.Create( "DButton", MainPanel )
|
||||
ClearMapProps:SetText( " Clear map props in the DB " )
|
||||
ClearMapProps:SetFont("pp_font")
|
||||
ClearMapProps:SetSize( 370, 30)
|
||||
ClearMapProps:SetPos( 10, 200 )
|
||||
ClearMapProps:SetTextColor( Color( 50, 50, 50, 255 ) )
|
||||
ClearMapProps.DoClick = function()
|
||||
|
||||
Derma_Query("Are you sure you want clear map props in the db ?\nYou can't undo this action !", "PermaProps 4.0", "Yes", function() net.Start("pp_info_send") net.WriteTable({CMD = "DEL_MAP"}) net.SendToServer() end, "Cancel")
|
||||
|
||||
end
|
||||
ClearMapProps.Paint = function(self)
|
||||
surface.SetDrawColor(50, 50, 50, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
end
|
||||
|
||||
local ClearAllProps = vgui.Create( "DButton", MainPanel )
|
||||
ClearAllProps:SetText( " Clear all props in the DB " )
|
||||
ClearAllProps:SetFont("pp_font")
|
||||
ClearAllProps:SetSize( 370, 30)
|
||||
ClearAllProps:SetPos( 10, 240 )
|
||||
ClearAllProps:SetTextColor( Color( 50, 50, 50, 255 ) )
|
||||
ClearAllProps.DoClick = function()
|
||||
|
||||
Derma_Query("Are you sure you want clear all props in the db ?\nYou can't undo this action !", "PermaProps 4.0", "Yes", function() net.Start("pp_info_send") net.WriteTable({CMD = "DEL_ALL"}) net.SendToServer() end, "Cancel")
|
||||
|
||||
end
|
||||
ClearAllProps.Paint = function(self)
|
||||
surface.SetDrawColor(50, 50, 50, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
end
|
||||
|
||||
local BMain = vgui.Create("DButton", Main)
|
||||
BSelect = BMain
|
||||
BMain:SetText("Main")
|
||||
BMain:SetFont("pp_font")
|
||||
BMain:SetSize(160, 50)
|
||||
BMain:SetPos(15, 27 + 25)
|
||||
BMain:SetTextColor( Color( 255, 255, 255, 255 ) )
|
||||
BMain.PaintColor = Color(17, 148, 240, 100)
|
||||
BMain.Paint = function(self)
|
||||
|
||||
draw.RoundedBox(0, 0, 0, self:GetWide(), self:GetTall(), self.PaintColor)
|
||||
surface.SetDrawColor(17, 148, 240, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
|
||||
end
|
||||
BMain.DoClick = function( self )
|
||||
|
||||
if BSelect then BSelect.PaintColor = Color(0, 0, 0, 0) end
|
||||
BSelect = self
|
||||
self.PaintColor = Color(17, 148, 240, 100)
|
||||
|
||||
if PSelect then PSelect:Hide() end
|
||||
MainPanel:Show()
|
||||
PSelect = MainPanel
|
||||
|
||||
end
|
||||
|
||||
local ConfigPanel = vgui.Create( "DPanel", Main )
|
||||
ConfigPanel:SetPos( 190, 51 )
|
||||
ConfigPanel:SetSize( 390, 275 )
|
||||
ConfigPanel.Paint = function( self )
|
||||
surface.SetDrawColor( 50, 50, 50, 200 )
|
||||
surface.DrawRect( 0, 0, self:GetWide(), self:GetTall() )
|
||||
end
|
||||
ConfigPanel:Hide()
|
||||
|
||||
local CheckCustom = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckCustom:SetPos( 5, 30 )
|
||||
CheckCustom:SetText( "Custom permissions" )
|
||||
CheckCustom:SetValue( 0 )
|
||||
CheckCustom:SizeToContents()
|
||||
CheckCustom:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckCustom:SetDisabled( true )
|
||||
|
||||
local GroupsList = vgui.Create( "DComboBox", ConfigPanel )
|
||||
GroupsList:SetPos( 5, 5 )
|
||||
GroupsList:SetSize( 125, 20 )
|
||||
GroupsList:SetValue( "Select a group..." )
|
||||
|
||||
local CheckBox1 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox1:SetPos( 150, 10 )
|
||||
CheckBox1:SetText( "Menu" )
|
||||
CheckBox1:SizeToContents()
|
||||
CheckBox1:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox1:SetDisabled( true )
|
||||
CheckBox1.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Menu", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox2 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox2:SetPos( 150, 30 )
|
||||
CheckBox2:SetText( "Edit permissions" )
|
||||
CheckBox2:SizeToContents()
|
||||
CheckBox2:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox2:SetDisabled( true )
|
||||
CheckBox2.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Permissions", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox3 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox3:SetPos( 150, 50 )
|
||||
CheckBox3:SetText( "Physgun permaprops" )
|
||||
CheckBox3:SizeToContents()
|
||||
CheckBox3:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox3:SetDisabled( true )
|
||||
CheckBox3.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Physgun", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox4 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox4:SetPos( 150, 70 )
|
||||
CheckBox4:SetText( "Tool permaprops" )
|
||||
CheckBox4:SizeToContents()
|
||||
CheckBox4:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox4:SetDisabled( true )
|
||||
CheckBox4.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Tool", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox5 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox5:SetPos( 150, 90 )
|
||||
CheckBox5:SetText( "Property permaprops" )
|
||||
CheckBox5:SizeToContents()
|
||||
CheckBox5:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox5:SetDisabled( true )
|
||||
CheckBox5.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Property", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox6 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox6:SetPos( 150, 110 )
|
||||
CheckBox6:SetText( "Save props" )
|
||||
CheckBox6:SizeToContents()
|
||||
CheckBox6:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox6:SetDisabled( true )
|
||||
CheckBox6.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Save", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox7 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox7:SetPos( 150, 130 )
|
||||
CheckBox7:SetText( "Delete permaprops" )
|
||||
CheckBox7:SizeToContents()
|
||||
CheckBox7:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox7:SetDisabled( true )
|
||||
CheckBox7.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Delete", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local CheckBox8 = vgui.Create( "DCheckBoxLabel", ConfigPanel )
|
||||
CheckBox8:SetPos( 150, 150 )
|
||||
CheckBox8:SetText( "Update permaprops" )
|
||||
CheckBox8:SizeToContents()
|
||||
CheckBox8:SetTextColor( Color( 0, 0, 0, 255) )
|
||||
CheckBox8:SetDisabled( true )
|
||||
CheckBox8.OnChange = function(Self, Value)
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Update", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
GroupsList.OnSelect = function( panel, index, value )
|
||||
|
||||
CheckCustom:SetDisabled( false )
|
||||
CheckCustom:SetChecked( Content.Permissions[value].Custom )
|
||||
|
||||
CheckBox1:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox1:SetChecked( Content.Permissions[value].Menu )
|
||||
CheckBox2:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox2:SetChecked( Content.Permissions[value].Permissions )
|
||||
CheckBox3:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox3:SetChecked( Content.Permissions[value].Physgun )
|
||||
CheckBox4:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox4:SetChecked( Content.Permissions[value].Tool )
|
||||
CheckBox5:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox5:SetChecked( Content.Permissions[value].Property )
|
||||
CheckBox6:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox6:SetChecked( Content.Permissions[value].Save )
|
||||
CheckBox7:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox7:SetChecked( Content.Permissions[value].Delete )
|
||||
CheckBox8:SetDisabled( !Content.Permissions[value].Custom )
|
||||
CheckBox8:SetChecked( Content.Permissions[value].Update )
|
||||
|
||||
end
|
||||
|
||||
for k, v in pairs(Content.Permissions) do
|
||||
|
||||
GroupsList:AddChoice(k)
|
||||
|
||||
end
|
||||
|
||||
CheckCustom.OnChange = function(Self, Value)
|
||||
|
||||
CheckBox1:SetDisabled( !Value )
|
||||
CheckBox2:SetDisabled( !Value )
|
||||
CheckBox3:SetDisabled( !Value )
|
||||
CheckBox4:SetDisabled( !Value )
|
||||
CheckBox5:SetDisabled( !Value )
|
||||
CheckBox6:SetDisabled( !Value )
|
||||
CheckBox7:SetDisabled( !Value )
|
||||
CheckBox8:SetDisabled( !Value )
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "VAR", Val = Value, Data = "Custom", Name = GroupsList:GetValue()})
|
||||
net.SendToServer()
|
||||
|
||||
end
|
||||
|
||||
local BConfig = vgui.Create("DButton", Main)
|
||||
BConfig:SetText("Configuration")
|
||||
BConfig:SetFont("pp_font")
|
||||
BConfig:SetSize(160, 50)
|
||||
BConfig:SetPos(15, 71 + 55)
|
||||
BConfig:SetTextColor( Color( 255, 255, 255, 255 ) )
|
||||
BConfig.PaintColor = Color(0, 0, 0, 0)
|
||||
BConfig.Paint = function(self)
|
||||
draw.RoundedBox(0, 0, 0, self:GetWide(), self:GetTall(), self.PaintColor)
|
||||
surface.SetDrawColor(17, 148, 240, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
end
|
||||
BConfig.DoClick = function( self )
|
||||
|
||||
if BSelect then BSelect.PaintColor = Color(0, 0, 0, 0) end
|
||||
BSelect = self
|
||||
self.PaintColor = Color(17, 148, 240, 100)
|
||||
|
||||
if PSelect then PSelect:Hide() end
|
||||
ConfigPanel:Show()
|
||||
PSelect = ConfigPanel
|
||||
|
||||
end
|
||||
|
||||
local PropsPanel = vgui.Create( "DPanel", Main )
|
||||
PropsPanel:SetPos( 190, 51 )
|
||||
PropsPanel:SetSize( 390, 275 )
|
||||
PropsPanel.Paint = function( self )
|
||||
surface.SetDrawColor( 50, 50, 50, 200 )
|
||||
surface.DrawRect( 0, 0, self:GetWide(), self:GetTall() )
|
||||
end
|
||||
PropsPanel:Hide()
|
||||
|
||||
local PropsList = vgui.Create( "DListView", PropsPanel )
|
||||
PropsList:SetMultiSelect( false )
|
||||
PropsList:SetSize( 390, 275 )
|
||||
local ColID = PropsList:AddColumn( "ID" )
|
||||
local ColEnt = PropsList:AddColumn( "Entity" )
|
||||
local ColMdl = PropsList:AddColumn( "Model" )
|
||||
ColID:SetMinWidth(50)
|
||||
ColID:SetMaxWidth(50)
|
||||
PropsList.Paint = function( self )
|
||||
surface.SetDrawColor(17, 148, 240, 255)
|
||||
end
|
||||
|
||||
PropsList.OnRowRightClick = function(panel, line)
|
||||
|
||||
local MenuButtonOptions = DermaMenu()
|
||||
MenuButtonOptions:AddOption("Draw entity", function()
|
||||
|
||||
if not LocalPlayer().DrawPPEnt or not istable(LocalPlayer().DrawPPEnt) then LocalPlayer().DrawPPEnt = {} end
|
||||
|
||||
if LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] and LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)]:IsValid() then return end
|
||||
|
||||
local ent = ents.CreateClientProp( Content.PropsList[PropsList:GetLine(line):GetValue(1)].Model )
|
||||
ent:SetPos( Content.PropsList[PropsList:GetLine(line):GetValue(1)].Pos )
|
||||
ent:SetAngles( Content.PropsList[PropsList:GetLine(line):GetValue(1)].Angle )
|
||||
|
||||
LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] = ent
|
||||
|
||||
end )
|
||||
|
||||
if LocalPlayer().DrawPPEnt and LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] then
|
||||
|
||||
MenuButtonOptions:AddOption("Stop Drawing", function()
|
||||
|
||||
LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)]:Remove()
|
||||
LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] = nil
|
||||
|
||||
end )
|
||||
|
||||
end
|
||||
|
||||
if LocalPlayer().DrawPPEnt != nil and istable(LocalPlayer().DrawPPEnt) and table.Count(LocalPlayer().DrawPPEnt) > 0 then
|
||||
|
||||
MenuButtonOptions:AddOption("Stop Drawing All", function()
|
||||
|
||||
for k, v in pairs(LocalPlayer().DrawPPEnt) do
|
||||
|
||||
LocalPlayer().DrawPPEnt[k]:Remove()
|
||||
LocalPlayer().DrawPPEnt[k] = nil
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
end
|
||||
|
||||
MenuButtonOptions:AddOption("Remove", function()
|
||||
|
||||
net.Start("pp_info_send")
|
||||
net.WriteTable({CMD = "DEL", Val = PropsList:GetLine(line):GetValue(1)})
|
||||
net.SendToServer()
|
||||
|
||||
if LocalPlayer().DrawPPEnt and LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] != nil then
|
||||
|
||||
LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)]:Remove()
|
||||
LocalPlayer().DrawPPEnt[PropsList:GetLine(line):GetValue(1)] = nil
|
||||
|
||||
end
|
||||
|
||||
PropsList:RemoveLine(line)
|
||||
|
||||
|
||||
end )
|
||||
MenuButtonOptions:Open()
|
||||
|
||||
end
|
||||
|
||||
for k, v in pairs(Content.PropsList) do
|
||||
|
||||
PropsList:AddLine(k, v.Class, v.Model)
|
||||
|
||||
end
|
||||
|
||||
local BProps = vgui.Create("DButton", Main)
|
||||
BProps:SetText("Props List")
|
||||
BProps:SetFont("pp_font")
|
||||
BProps:SetSize(160, 50)
|
||||
BProps:SetPos(15, 115 + 85)
|
||||
BProps:SetTextColor( Color( 255, 255, 255, 255 ) )
|
||||
BProps.PaintColor = Color(0, 0, 0, 0)
|
||||
BProps.Paint = function(self)
|
||||
draw.RoundedBox(0, 0, 0, self:GetWide(), self:GetTall(), self.PaintColor)
|
||||
surface.SetDrawColor(17, 148, 240, 255)
|
||||
surface.DrawOutlinedRect(0, 0, self:GetWide(), self:GetTall())
|
||||
end
|
||||
BProps.DoClick = function( self )
|
||||
|
||||
if BSelect then BSelect.PaintColor = Color(0, 0, 0, 0) end
|
||||
BSelect = self
|
||||
self.PaintColor = Color(17, 148, 240, 100)
|
||||
|
||||
if PSelect then PSelect:Hide() end
|
||||
PropsPanel:Show()
|
||||
PSelect = PropsPanel
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
net.Receive("pp_open_menu", pp_open_menu)
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
if not PermaProps then PermaProps = {} end
|
||||
|
||||
function PermaProps.PPGetEntTable( ent )
|
||||
|
||||
if !ent or !ent:IsValid() then return false end
|
||||
|
||||
local content = {}
|
||||
content.Class = ent:GetClass()
|
||||
content.Pos = ent:GetPos()
|
||||
content.Angle = ent:GetAngles()
|
||||
content.Model = ent:GetModel()
|
||||
content.Skin = ent:GetSkin()
|
||||
//content.Mins, content.Maxs = ent:GetCollisionBounds()
|
||||
content.ColGroup = ent:GetCollisionGroup()
|
||||
content.Name = ent:GetName()
|
||||
content.ModelScale = ent:GetModelScale()
|
||||
content.Color = ent:GetColor()
|
||||
content.Material = ent:GetMaterial()
|
||||
content.Solid = ent:GetSolid()
|
||||
content.RenderMode = ent:GetRenderMode()
|
||||
|
||||
if PermaProps.SpecialENTSSave[ent:GetClass()] != nil and isfunction(PermaProps.SpecialENTSSave[ent:GetClass()]) then
|
||||
|
||||
local othercontent = PermaProps.SpecialENTSSave[ent:GetClass()](ent)
|
||||
if not othercontent then return false end
|
||||
if othercontent != nil and istable(othercontent) then
|
||||
table.Merge(content, othercontent)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
do
|
||||
local othercontent = hook.Run("PermaProps.OnEntitySaved", ent)
|
||||
if othercontent and istable(othercontent) then
|
||||
table.Merge(content, othercontent)
|
||||
end
|
||||
end
|
||||
|
||||
if ( ent.GetNetworkVars ) then
|
||||
content.DT = ent:GetNetworkVars()
|
||||
end
|
||||
|
||||
local sm = ent:GetMaterials()
|
||||
if ( sm and istable(sm) ) then
|
||||
|
||||
for k, v in pairs( sm ) do
|
||||
|
||||
if ( ent:GetSubMaterial( k )) then
|
||||
|
||||
content.SubMat = content.SubMat or {}
|
||||
content.SubMat[ k ] = ent:GetSubMaterial( k-1 )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local bg = ent:GetBodyGroups()
|
||||
if ( bg ) then
|
||||
|
||||
for k, v in pairs( bg ) do
|
||||
|
||||
if ( ent:GetBodygroup( v.id ) > 0 ) then
|
||||
|
||||
content.BodyG = content.BodyG or {}
|
||||
content.BodyG[ v.id ] = ent:GetBodygroup( v.id )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if ent:GetPhysicsObject() and ent:GetPhysicsObject():IsValid() then
|
||||
content.Frozen = !ent:GetPhysicsObject():IsMoveable()
|
||||
end
|
||||
|
||||
if content.Class == "prop_dynamic" then
|
||||
content.Class = "prop_physics"
|
||||
end
|
||||
|
||||
--content.Table = PermaProps.UselessContent( ent:GetTable() )
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.PPEntityFromTable( data, id )
|
||||
|
||||
if not id or not isnumber(id) then return false end
|
||||
if not data or not istable(data) then return false end
|
||||
|
||||
if data.Class == "prop_physics" and data.Frozen then
|
||||
data.Class = "prop_dynamic" -- Can reduce lags
|
||||
end
|
||||
|
||||
local ent = ents.Create(data.Class)
|
||||
if !ent then return false end
|
||||
if !ent:IsVehicle() then if !ent:IsValid() then return false end end
|
||||
ent:SetPos( data.Pos or Vector(0, 0, 0) )
|
||||
ent:SetAngles( data.Angle or Angle(0, 0, 0) )
|
||||
ent:SetModel( data.Model or "models/error.mdl" )
|
||||
ent:SetSkin( data.Skin or 0 )
|
||||
//ent:SetCollisionBounds( ( data.Mins or 0 ), ( data.Maxs or 0 ) )
|
||||
ent:SetCollisionGroup( data.ColGroup or 0 )
|
||||
ent:SetName( data.Name or "" )
|
||||
ent:SetModelScale( data.ModelScale or 1 )
|
||||
ent:SetMaterial( data.Material or "" )
|
||||
ent:SetSolid( data.Solid or 6 )
|
||||
|
||||
if PermaProps.SpecialENTSSpawn[data.Class] != nil and isfunction(PermaProps.SpecialENTSSpawn[data.Class]) then
|
||||
|
||||
PermaProps.SpecialENTSSpawn[data.Class](ent, data.Other)
|
||||
|
||||
else
|
||||
|
||||
ent:Spawn()
|
||||
|
||||
end
|
||||
|
||||
hook.Run("PermaProps.OnEntityCreated", ent, data)
|
||||
|
||||
ent:SetRenderMode( data.RenderMode or RENDERMODE_NORMAL )
|
||||
ent:SetColor( data.Color or Color(255, 255, 255, 255) )
|
||||
|
||||
if data.EntityMods != nil and istable(data.EntityMods) then -- OLD DATA
|
||||
|
||||
if data.EntityMods.material then
|
||||
ent:SetMaterial( data.EntityMods.material["MaterialOverride"] or "")
|
||||
end
|
||||
|
||||
if data.EntityMods.colour then
|
||||
ent:SetColor( data.EntityMods.colour.Color or Color(255, 255, 255, 255))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if data.DT then
|
||||
|
||||
for k, v in pairs( data.DT ) do
|
||||
|
||||
if ( data.DT[ k ] == nil ) then continue end
|
||||
if !isfunction(ent[ "Set" .. k ]) then continue end
|
||||
ent[ "Set" .. k ]( ent, data.DT[ k ] )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if data.BodyG then
|
||||
|
||||
for k, v in pairs( data.BodyG ) do
|
||||
|
||||
ent:SetBodygroup( k, v )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
if data.SubMat then
|
||||
|
||||
for k, v in pairs( data.SubMat ) do
|
||||
|
||||
if type(k) != "number" or type(v) != "string" then continue end
|
||||
|
||||
ent:SetSubMaterial( k-1, v )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if data.Frozen != nil then
|
||||
|
||||
local phys = ent:GetPhysicsObject()
|
||||
if phys and phys:IsValid() then
|
||||
phys:EnableMotion(!data.Frozen)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
/*if data.Table then
|
||||
|
||||
table.Merge(ent:GetTable(), data.Table)
|
||||
|
||||
end*/
|
||||
|
||||
ent.PermaProps_ID = id
|
||||
ent.PermaProps = true
|
||||
|
||||
// For all idiots who don't know how to config FPP, FUCK YOU
|
||||
function ent:CanTool( ply, trace, tool )
|
||||
|
||||
if trace and IsValid(trace.Entity) and trace.Entity.PermaProps then
|
||||
|
||||
if tool == "permaprops" then
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
return PermaProps.HasPermission( ply, "Tool")
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return ent
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.ReloadPermaProps()
|
||||
|
||||
for k, v in pairs( ents.GetAll() ) do
|
||||
|
||||
if v.PermaProps == true then
|
||||
|
||||
v:Remove()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local content = PermaProps.SQL.Query( "SELECT * FROM permaprops WHERE map = ".. sql.SQLStr(game.GetMap()) .. ";" )
|
||||
|
||||
if not content or content == nil then return end
|
||||
|
||||
for k, v in pairs( content ) do
|
||||
|
||||
local data = util.JSONToTable(v.content)
|
||||
|
||||
local e = PermaProps.PPEntityFromTable(data, tonumber(v.id))
|
||||
if !e or !e:IsValid() then continue end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
hook.Add("InitPostEntity", "InitializePermaProps", PermaProps.ReloadPermaProps)
|
||||
hook.Add("PostCleanupMap", "WhenCleanUpPermaProps", PermaProps.ReloadPermaProps) -- #MOMO
|
||||
timer.Simple(5, function() PermaProps.ReloadPermaProps() end) -- When the hook isn't call ...
|
||||
|
||||
function PermaProps.SparksEffect( ent )
|
||||
|
||||
local effectdata = EffectData()
|
||||
effectdata:SetOrigin(ent:GetPos())
|
||||
effectdata:SetMagnitude(2.5)
|
||||
effectdata:SetScale(2)
|
||||
effectdata:SetRadius(3)
|
||||
util.Effect("Sparks", effectdata, true, true)
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.IsUserGroup( ply, name )
|
||||
|
||||
if not ply:IsValid() then return false end
|
||||
return ply:GetNetworkedString("UserGroup") == name
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.IsAdmin( ply )
|
||||
|
||||
if ( PermaProps.IsUserGroup(ply, "superadmin") or false ) then return true end
|
||||
if ( PermaProps.IsUserGroup(ply, "admin") or false ) then return true end
|
||||
|
||||
return false
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.IsSuperAdmin( ply )
|
||||
|
||||
return ( PermaProps.IsUserGroup(ply, "superadmin") or false )
|
||||
|
||||
end
|
||||
|
||||
function PermaProps.UselessContent( tbl )
|
||||
|
||||
local function SortFcn( tbl2 )
|
||||
|
||||
for k, v in pairs( tbl2 ) do
|
||||
|
||||
if isfunction( v ) or isentity( v ) then
|
||||
|
||||
tbl2[k] = nil
|
||||
|
||||
elseif istable( v ) then
|
||||
|
||||
SortFcn( v )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return tbl2
|
||||
|
||||
end
|
||||
|
||||
for k, v in pairs( tbl ) do
|
||||
|
||||
if isfunction( v ) or isentity( v ) then
|
||||
|
||||
tbl[k] = nil
|
||||
|
||||
elseif istable( v ) then
|
||||
|
||||
SortFcn( v )
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return tbl
|
||||
|
||||
end
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
util.AddNetworkString("pp_open_menu")
|
||||
util.AddNetworkString("pp_info_send")
|
||||
|
||||
local function PermissionLoad()
|
||||
|
||||
if not PermaProps then PermaProps = {} end
|
||||
if not PermaProps.Permissions then PermaProps.Permissions = {} end
|
||||
|
||||
PermaProps.Permissions["superadmin"] = { Physgun = true, Tool = true, Property = true, Save = true, Delete = true, Update = true, Menu = true, Permissions = true, Inherits = "admin", Custom = true }
|
||||
PermaProps.Permissions["admin"] = { Physgun = false, Tool = false, Property = false, Save = true, Delete = true, Update = true, Menu = true, Permissions = false, Inherits = "user", Custom = true }
|
||||
PermaProps.Permissions["user"] = { Physgun = false, Tool = false, Property = false, Save = false, Delete = false, Update = false, Menu = false, Permissions = false, Inherits = "user", Custom = true }
|
||||
|
||||
if CAMI then
|
||||
|
||||
for k, v in pairs(CAMI.GetUsergroups()) do
|
||||
|
||||
if k == "superadmin" or k == "admin" or k == "user" then continue end
|
||||
|
||||
PermaProps.Permissions[k] = { Physgun = false, Tool = false, Property = false, Save = false, Delete = false, Update = false, Menu = false, Permissions = false, Inherits = v.Inherits, Custom = false }
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if file.Exists( "permaprops_config.txt", "DATA" ) then
|
||||
file.Delete( "permaprops_config.txt" )
|
||||
end
|
||||
|
||||
if file.Exists( "permaprops_permissions.txt", "DATA" ) then
|
||||
|
||||
local content = file.Read("permaprops_permissions.txt", "DATA")
|
||||
local tablecontent = util.JSONToTable( content )
|
||||
|
||||
for k, v in pairs(tablecontent) do
|
||||
|
||||
if PermaProps.Permissions[k] == nil then
|
||||
|
||||
tablecontent[k] = nil
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
table.Merge(PermaProps.Permissions, ( tablecontent or {} ))
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
hook.Add("Initialize", "PermaPropPermLoad", PermissionLoad)
|
||||
hook.Add("CAMI.OnUsergroupRegistered", "PermaPropPermLoadCAMI", PermissionLoad) -- In case something changes
|
||||
|
||||
local function PermissionSave()
|
||||
|
||||
file.Write( "permaprops_permissions.txt", util.TableToJSON(PermaProps.Permissions) )
|
||||
|
||||
end
|
||||
|
||||
local function pp_open_menu( ply )
|
||||
|
||||
if !PermaProps.HasPermission( ply, "Menu") then ply:ChatPrint("Access denied !") return end
|
||||
|
||||
local SendTable = {}
|
||||
local Data_PropsList = sql.Query( "SELECT * FROM permaprops WHERE map = ".. sql.SQLStr(game.GetMap()) .. ";" )
|
||||
|
||||
if Data_PropsList and #Data_PropsList < 200 then
|
||||
|
||||
for k, v in pairs( Data_PropsList ) do
|
||||
|
||||
local data = util.JSONToTable(v.content)
|
||||
|
||||
SendTable[v.id] = {Model = data.Model, Class = data.Class, Pos = data.Pos, Angle = data.Angle}
|
||||
|
||||
end
|
||||
|
||||
elseif Data_PropsList and #Data_PropsList > 200 then -- Too much props dude :'(
|
||||
|
||||
for i = 1, 199 do
|
||||
|
||||
local data = util.JSONToTable(Data_PropsList[i].content)
|
||||
|
||||
SendTable[Data_PropsList[i].id] = {Model = data.Model, Class = data.Class, Pos = data.Pos, Angle = data.Angle}
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local Content = {}
|
||||
Content.MProps = tonumber(sql.QueryValue("SELECT COUNT(*) FROM permaprops WHERE map = ".. sql.SQLStr(game.GetMap()) .. ";"))
|
||||
Content.TProps = tonumber(sql.QueryValue("SELECT COUNT(*) FROM permaprops;"))
|
||||
|
||||
Content.PropsList = SendTable
|
||||
Content.Permissions = PermaProps.Permissions
|
||||
|
||||
local Data = util.TableToJSON( Content )
|
||||
local Compressed = util.Compress( Data )
|
||||
|
||||
net.Start( "pp_open_menu" )
|
||||
net.WriteFloat( Compressed:len() )
|
||||
net.WriteData( Compressed, Compressed:len() )
|
||||
net.Send( ply )
|
||||
|
||||
end
|
||||
concommand.Add("pp_cfg_open", pp_open_menu)
|
||||
|
||||
local function pp_info_send( um, ply )
|
||||
|
||||
if !PermaProps.HasPermission( ply, "Menu") then ply:ChatPrint("Access denied !") return end
|
||||
|
||||
local Content = net.ReadTable()
|
||||
|
||||
if Content["CMD"] == "DEL" then
|
||||
|
||||
Content["Val"] = tonumber(Content["Val"])
|
||||
|
||||
if Content["Val"] != nil and Content["Val"] <= 0 then return end
|
||||
|
||||
sql.Query("DELETE FROM permaprops WHERE id = ".. sql.SQLStr(Content["Val"]) .. ";")
|
||||
|
||||
for k, v in pairs(ents.GetAll()) do
|
||||
|
||||
if v.PermaProps_ID == Content["Val"] then
|
||||
|
||||
ply:ChatPrint("You erased " .. v:GetClass() .. " with a model of " .. v:GetModel() .. " from the database.")
|
||||
v:Remove()
|
||||
break
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
elseif Content["CMD"] == "VAR" then
|
||||
|
||||
if PermaProps.Permissions[Content["Name"]] == nil or PermaProps.Permissions[Content["Name"]][Content["Data"]] == nil then return end
|
||||
if !isbool(Content["Val"]) then return end
|
||||
|
||||
if Content["Name"] == "superadmin" and ( Content["Data"] == "Custom" or Content["Data"] == "Permissions" or Content["Data"] == "Menu" ) then return end
|
||||
|
||||
if !PermaProps.HasPermission( ply, "Permissions") then ply:ChatPrint("Access denied !") return end
|
||||
|
||||
PermaProps.Permissions[Content["Name"]][Content["Data"]] = Content["Val"]
|
||||
|
||||
PermissionSave()
|
||||
|
||||
elseif Content["CMD"] == "DEL_MAP" then
|
||||
|
||||
sql.Query( "DELETE FROM permaprops WHERE map = ".. sql.SQLStr(game.GetMap()) .. ";" )
|
||||
PermaProps.ReloadPermaProps()
|
||||
|
||||
ply:ChatPrint("You erased all props from the map !")
|
||||
|
||||
elseif Content["CMD"] == "DEL_ALL" then
|
||||
|
||||
sql.Query( "DELETE FROM permaprops;" )
|
||||
PermaProps.ReloadPermaProps()
|
||||
|
||||
ply:ChatPrint("You erased all props !")
|
||||
|
||||
elseif Content["CMD"] == "CLR_MAP" then
|
||||
|
||||
for k, v in pairs( ents.GetAll() ) do
|
||||
|
||||
if v.PermaProps == true then
|
||||
|
||||
v:Remove()
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
ply:ChatPrint("You have removed all props !")
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
net.Receive("pp_info_send", pp_info_send)
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
Thanks to ARitz Cracker for this part
|
||||
*/
|
||||
|
||||
function PermaProps.HasPermission( ply, name )
|
||||
|
||||
if !PermaProps or !PermaProps.Permissions or !PermaProps.Permissions[ply:GetUserGroup()] then return false end
|
||||
|
||||
if PermaProps.Permissions[ply:GetUserGroup()].Custom == false and PermaProps.Permissions[ply:GetUserGroup()].Inherits and PermaProps.Permissions[PermaProps.Permissions[ply:GetUserGroup()].Inherits] then
|
||||
|
||||
return PermaProps.Permissions[PermaProps.Permissions[ply:GetUserGroup()].Inherits][name]
|
||||
|
||||
end
|
||||
|
||||
return PermaProps.Permissions[ply:GetUserGroup()][name]
|
||||
|
||||
end
|
||||
|
||||
local function PermaPropsPhys( ply, ent, phys )
|
||||
|
||||
if ent.PermaProps then
|
||||
|
||||
return PermaProps.HasPermission( ply, "Physgun")
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
hook.Add("PhysgunPickup", "PermaPropsPhys", PermaPropsPhys)
|
||||
hook.Add( "CanPlayerUnfreeze", "PermaPropsUnfreeze", PermaPropsPhys) -- Prevents people from pressing RELOAD on the physgun
|
||||
|
||||
local function PermaPropsTool( ply, tr, tool )
|
||||
|
||||
if IsValid(tr.Entity) then
|
||||
|
||||
if tr.Entity.PermaProps then
|
||||
|
||||
if tool == "permaprops" then
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
return PermaProps.HasPermission( ply, "Tool")
|
||||
|
||||
end
|
||||
|
||||
if tr.Entity:GetClass() == "sammyservers_textscreen" and tool == "permaprops" then -- Let people use PermaProps on textscreen
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
hook.Add( "CanTool", "PermaPropsTool", PermaPropsTool)
|
||||
|
||||
local function PermaPropsProperty( ply, property, ent )
|
||||
|
||||
if IsValid(ent) and ent.PermaProps and tool ~= "permaprops" then
|
||||
|
||||
return PermaProps.HasPermission( ply, "Property")
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
hook.Add( "CanProperty", "PermaPropsProperty", PermaPropsProperty)
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
if not PermaProps then PermaProps = {} end
|
||||
|
||||
PermaProps.SpecialENTSSpawn = {}
|
||||
PermaProps.SpecialENTSSpawn["gmod_lamp"] = function( ent, data)
|
||||
|
||||
ent:SetFlashlightTexture( data["Texture"] )
|
||||
ent:SetLightFOV( data["fov"] )
|
||||
ent:SetColor( Color( data["r"], data["g"], data["b"], 255 ) )
|
||||
ent:SetDistance( data["distance"] )
|
||||
ent:SetBrightness( data["brightness"] )
|
||||
ent:Switch( true )
|
||||
|
||||
ent:Spawn()
|
||||
|
||||
ent.Texture = data["Texture"]
|
||||
ent.KeyDown = data["KeyDown"]
|
||||
ent.fov = data["fov"]
|
||||
ent.distance = data["distance"]
|
||||
ent.r = data["r"]
|
||||
ent.g = data["g"]
|
||||
ent.b = data["b"]
|
||||
ent.brightness = data["brightness"]
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSpawn["prop_vehicle_jeep"] = function( ent, data)
|
||||
|
||||
if ( ent:GetModel() == "models/buggy.mdl" ) then ent:SetKeyValue( "vehiclescript", "scripts/vehicles/jeep_test.txt" ) end
|
||||
if ( ent:GetModel() == "models/vehicle.mdl" ) then ent:SetKeyValue( "vehiclescript", "scripts/vehicles/jalopy.txt" ) end
|
||||
|
||||
if ( data["VehicleTable"] && data["VehicleTable"].KeyValues ) then
|
||||
for k, v in pairs( data["VehicleTable"].KeyValues ) do
|
||||
ent:SetKeyValue( k, v )
|
||||
end
|
||||
end
|
||||
|
||||
ent:Spawn()
|
||||
ent:Activate()
|
||||
|
||||
ent:SetVehicleClass( data["VehicleName"] )
|
||||
ent.VehicleName = data["VehicleName"]
|
||||
ent.VehicleTable = data["VehicleTable"]
|
||||
ent.ClassOverride = data["Class"]
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
PermaProps.SpecialENTSSpawn["prop_vehicle_jeep_old"] = PermaProps.SpecialENTSSpawn["prop_vehicle_jeep"]
|
||||
PermaProps.SpecialENTSSpawn["prop_vehicle_airboat"] = PermaProps.SpecialENTSSpawn["prop_vehicle_jeep"]
|
||||
PermaProps.SpecialENTSSpawn["prop_vehicle_prisoner_pod"] = PermaProps.SpecialENTSSpawn["prop_vehicle_jeep"]
|
||||
|
||||
PermaProps.SpecialENTSSpawn["prop_ragdoll"] = function( ent, data )
|
||||
|
||||
if !data or !istable( data ) then return end
|
||||
|
||||
ent:Spawn()
|
||||
ent:Activate()
|
||||
|
||||
if data["Bones"] then
|
||||
|
||||
for objectid, objectdata in pairs( data["Bones"] ) do
|
||||
|
||||
local Phys = ent:GetPhysicsObjectNum( objectid )
|
||||
if !IsValid( Phys ) then continue end
|
||||
|
||||
if ( isvector( objectdata.Pos ) && isangle( objectdata.Angle ) ) then
|
||||
|
||||
local pos, ang = LocalToWorld( objectdata.Pos, objectdata.Angle, Vector(0, 0, 0), Angle(0, 0, 0) )
|
||||
Phys:SetPos( pos )
|
||||
Phys:SetAngles( ang )
|
||||
Phys:Wake()
|
||||
|
||||
if objectdata.Frozen then
|
||||
Phys:EnableMotion( false )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if data["BoneManip"] and ent:IsValid() then
|
||||
|
||||
for k, v in pairs( data["BoneManip"] ) do
|
||||
|
||||
if ( v.s ) then ent:ManipulateBoneScale( k, v.s ) end
|
||||
if ( v.a ) then ent:ManipulateBoneAngles( k, v.a ) end
|
||||
if ( v.p ) then ent:ManipulateBonePosition( k, v.p ) end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if data["Flex"] and ent:IsValid() then
|
||||
|
||||
for k, v in pairs( data["Flex"] ) do
|
||||
ent:SetFlexWeight( k, v )
|
||||
end
|
||||
|
||||
if ( Scale ) then
|
||||
ent:SetFlexScale( Scale )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSpawn["sammyservers_textscreen"] = function( ent, data )
|
||||
|
||||
if !data or !istable( data ) then return end
|
||||
|
||||
ent:Spawn()
|
||||
ent:Activate()
|
||||
|
||||
if data["Lines"] then
|
||||
|
||||
for k, v in pairs(data["Lines"] or {}) do
|
||||
|
||||
ent:SetLine(k, v.text, Color(v.color.r, v.color.g, v.color.b, v.color.a), v.size, v.font, v.rainbow or 0)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSpawn["NPC"] = function( ent, data )
|
||||
|
||||
if data and istable( data ) then
|
||||
|
||||
if data["Equipment"] then
|
||||
|
||||
local valid = false
|
||||
for _, v in pairs( list.Get( "NPCUsableWeapons" ) ) do
|
||||
if v.class == data["Equipment"] then valid = true break end
|
||||
end
|
||||
|
||||
if ( data["Equipment"] && data["Equipment"] != "none" && valid ) then
|
||||
ent:SetKeyValue( "additionalequipment", data["Equipment"] )
|
||||
ent.Equipment = data["Equipment"]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
ent:Spawn()
|
||||
ent:Activate()
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
if list.Get( "NPC" ) and istable(list.Get( "NPC" )) then
|
||||
|
||||
for k, v in pairs(list.Get( "NPC" )) do
|
||||
|
||||
PermaProps.SpecialENTSSpawn[k] = PermaProps.SpecialENTSSpawn["NPC"]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSpawn["item_ammo_crate"] = function( ent, data )
|
||||
|
||||
if data and istable(data) and data["type"] then
|
||||
|
||||
ent.type = data["type"]
|
||||
ent:SetKeyValue( "AmmoType", math.Clamp( data["type"], 0, 9 ) )
|
||||
|
||||
end
|
||||
|
||||
ent:Spawn()
|
||||
ent:Activate()
|
||||
|
||||
return true
|
||||
|
||||
end
|
||||
|
||||
|
||||
PermaProps.SpecialENTSSave = {}
|
||||
PermaProps.SpecialENTSSave["gmod_lamp"] = function( ent )
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["Texture"] = ent.Texture
|
||||
content.Other["KeyDown"] = ent.KeyDown
|
||||
content.Other["fov"] = ent.fov
|
||||
content.Other["distance"] = ent.distance
|
||||
content.Other["r"] = ent.r
|
||||
content.Other["g"] = ent.g
|
||||
content.Other["b"] = ent.b
|
||||
content.Other["brightness"] = ent.brightness
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSave["prop_vehicle_jeep"] = function( ent )
|
||||
|
||||
if not ent.VehicleTable then return false end
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["VehicleName"] = ent.VehicleName
|
||||
content.Other["VehicleTable"] = ent.VehicleTable
|
||||
content.Other["ClassOverride"] = ent.ClassOverride
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
PermaProps.SpecialENTSSave["prop_vehicle_jeep_old"] = PermaProps.SpecialENTSSave["prop_vehicle_jeep"]
|
||||
PermaProps.SpecialENTSSave["prop_vehicle_airboat"] = PermaProps.SpecialENTSSave["prop_vehicle_jeep"]
|
||||
PermaProps.SpecialENTSSave["prop_vehicle_prisoner_pod"] = PermaProps.SpecialENTSSave["prop_vehicle_jeep"]
|
||||
|
||||
PermaProps.SpecialENTSSave["prop_ragdoll"] = function( ent )
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["Bones"] = {}
|
||||
|
||||
local num = ent:GetPhysicsObjectCount()
|
||||
for objectid = 0, num - 1 do
|
||||
|
||||
local obj = ent:GetPhysicsObjectNum( objectid )
|
||||
if ( !obj:IsValid() ) then continue end
|
||||
|
||||
content.Other["Bones"][ objectid ] = {}
|
||||
|
||||
content.Other["Bones"][ objectid ].Pos = obj:GetPos()
|
||||
content.Other["Bones"][ objectid ].Angle = obj:GetAngles()
|
||||
content.Other["Bones"][ objectid ].Frozen = !obj:IsMoveable()
|
||||
if ( obj:IsAsleep() ) then content.Other["Bones"][ objectid ].Sleep = true end
|
||||
|
||||
content.Other["Bones"][ objectid ].Pos, content.Other["Bones"][ objectid ].Angle = WorldToLocal( content.Other["Bones"][ objectid ].Pos, content.Other["Bones"][ objectid ].Angle, Vector( 0, 0, 0 ), Angle( 0, 0, 0 ) )
|
||||
|
||||
end
|
||||
|
||||
if ( ent:HasBoneManipulations() ) then
|
||||
|
||||
content.Other["BoneManip"] = {}
|
||||
|
||||
for i = 0, ent:GetBoneCount() do
|
||||
|
||||
local t = {}
|
||||
|
||||
local s = ent:GetManipulateBoneScale( i )
|
||||
local a = ent:GetManipulateBoneAngles( i )
|
||||
local p = ent:GetManipulateBonePosition( i )
|
||||
|
||||
if ( s != Vector( 1, 1, 1 ) ) then t[ 's' ] = s end -- scale
|
||||
if ( a != Angle( 0, 0, 0 ) ) then t[ 'a' ] = a end -- angle
|
||||
if ( p != Vector( 0, 0, 0 ) ) then t[ 'p' ] = p end -- position
|
||||
|
||||
if ( table.Count( t ) > 0 ) then
|
||||
content.Other["BoneManip"][ i ] = t
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
content.Other["FlexScale"] = ent:GetFlexScale()
|
||||
for i = 0, ent:GetFlexNum() do
|
||||
|
||||
local w = ent:GetFlexWeight( i )
|
||||
if ( w != 0 ) then
|
||||
content.Other["Flex"] = content.Other["Flex"] or {}
|
||||
content.Other["Flex"][ i ] = w
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSave["sammyservers_textscreen"] = function( ent )
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["Lines"] = ent.lines or {}
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSave["prop_effect"] = function( ent )
|
||||
|
||||
local content = {}
|
||||
content.Class = "pp_prop_effect"
|
||||
content.Model = ent.AttachedEntity:GetModel()
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
PermaProps.SpecialENTSSave["pp_prop_effect"] = PermaProps.SpecialENTSSave["prop_effect"]
|
||||
|
||||
PermaProps.SpecialENTSSave["NPC"] = function( ent )
|
||||
|
||||
if !ent.Equipment then return {} end
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["Equipment"] = ent.Equipment
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
|
||||
if list.Get( "NPC" ) and istable(list.Get( "NPC" )) then
|
||||
|
||||
for k, v in pairs(list.Get( "NPC" )) do
|
||||
|
||||
PermaProps.SpecialENTSSave[k] = PermaProps.SpecialENTSSave["NPC"]
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
PermaProps.SpecialENTSSave["item_ammo_crate"] = function( ent )
|
||||
|
||||
local content = {}
|
||||
content.Other = {}
|
||||
content.Other["type"] = ent.type
|
||||
|
||||
return content
|
||||
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
____ _ _ ____ __ __ _ _
|
||||
/ ___|___ __| | ___ __| | | __ ) _ _ | \/ | __ _| | |__ ___ _ __ ___
|
||||
| | / _ \ / _` |/ _ \/ _` | | _ \| | | | | |\/| |/ _` | | '_ \ / _ \| '__/ _ \
|
||||
| |__| (_) | (_| | __/ (_| | | |_) | |_| | | | | | (_| | | |_) | (_) | | | (_) |
|
||||
\____\___/ \__,_|\___|\__,_| |____/ \__, | |_| |_|\__,_|_|_.__/ \___/|_| \___/
|
||||
|___/
|
||||
*/
|
||||
|
||||
sql.Query("CREATE TABLE IF NOT EXISTS permaprops('id' INTEGER NOT NULL, 'map' TEXT NOT NULL, 'content' TEXT NOT NULL, PRIMARY KEY('id'));")
|
||||
|
||||
if not PermaProps then PermaProps = {} end
|
||||
|
||||
PermaProps.SQL = {}
|
||||
|
||||
/* NOT WORKS AT THE MOMENT
|
||||
PermaProps.SQL.MySQL = false
|
||||
PermaProps.SQL.Host = "127.0.0.1"
|
||||
PermaProps.SQL.Username = "username"
|
||||
PermaProps.SQL.Password = "password"
|
||||
PermaProps.SQL.Database_name = "PermaProps"
|
||||
PermaProps.SQL.Database_port = 3306
|
||||
PermaProps.SQL.Preferred_module = "mysqloo"
|
||||
*/
|
||||
|
||||
function PermaProps.SQL.Query( data )
|
||||
|
||||
return sql.Query( data )
|
||||
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
PLUGIN.name = "Build Tools"
|
||||
PLUGIN.author = "Various (Портирован для Helix)"
|
||||
PLUGIN.description = "Набор строительных инструментов: AdvDupe2, Permaprops, Precision Tool"
|
||||
|
||||
-- Устанавливаем высокий приоритет загрузки, чтобы плагин загрузился раньше инструментов
|
||||
PLUGIN.priority = 100
|
||||
|
||||
-- Конфигурация
|
||||
ix.config.Add("buildToolsEnabled", true, "Включить строительные инструменты", nil, {
|
||||
category = "Build Tools"
|
||||
})
|
||||
|
||||
ix.config.Add("buildToolsAdvDupe2", true, "Включить AdvDupe2 (дублирование)", nil, {
|
||||
category = "Build Tools"
|
||||
})
|
||||
|
||||
ix.config.Add("buildToolsPermaprops", true, "Включить Permaprops (постоянные пропы)", nil, {
|
||||
category = "Build Tools"
|
||||
})
|
||||
|
||||
ix.config.Add("buildToolsPrecision", true, "Включить Precision Tool (точное позиционирование)", nil, {
|
||||
category = "Build Tools"
|
||||
})
|
||||
|
||||
ix.config.Add("buildToolsNoCollideWorld", true, "Включить NoCollide World (отключение коллизий с миром)", nil, {
|
||||
category = "Build Tools"
|
||||
})
|
||||
|
||||
-- Загрузка NetStream (библиотека для сетевых сообщений)
|
||||
ix.util.Include("netstream.lua")
|
||||
|
||||
-- Загрузка AdvDupe2
|
||||
if ix.config.Get("buildToolsAdvDupe2", true) then
|
||||
-- Инициализация глобальной таблицы AdvDupe2
|
||||
AdvDupe2 = AdvDupe2 or {}
|
||||
print("[Build Tools] Initializing AdvDupe2...")
|
||||
|
||||
-- Shared
|
||||
ix.util.Include("advdupe2/sh_codec_legacy.lua")
|
||||
ix.util.Include("advdupe2/sh_codec.lua")
|
||||
|
||||
-- Server
|
||||
ix.util.Include("advdupe2/sv_file.lua")
|
||||
ix.util.Include("advdupe2/sv_ghost.lua")
|
||||
ix.util.Include("advdupe2/sv_clipboard.lua")
|
||||
ix.util.Include("advdupe2/sv_misc.lua")
|
||||
|
||||
-- Client
|
||||
ix.util.Include("advdupe2/cl_file.lua")
|
||||
ix.util.Include("advdupe2/cl_ghost.lua")
|
||||
ix.util.Include("advdupe2/file_browser.lua")
|
||||
|
||||
print("[Build Tools] AdvDupe2 loaded successfully")
|
||||
end
|
||||
|
||||
-- Загрузка Permaprops
|
||||
if ix.config.Get("buildToolsPermaprops", true) then
|
||||
-- Server
|
||||
ix.util.Include("permaprops/sv_lib.lua")
|
||||
ix.util.Include("permaprops/sv_menu.lua")
|
||||
ix.util.Include("permaprops/sv_permissions.lua")
|
||||
ix.util.Include("permaprops/sv_specialfcn.lua")
|
||||
ix.util.Include("permaprops/sv_sql.lua")
|
||||
|
||||
-- Client
|
||||
ix.util.Include("permaprops/cl_drawent.lua")
|
||||
ix.util.Include("permaprops/cl_menu.lua")
|
||||
end
|
||||
|
||||
function PLUGIN:Initialize()
|
||||
print("[Build Tools] Загружены строительные инструменты!")
|
||||
|
||||
if ix.config.Get("buildToolsAdvDupe2", true) then
|
||||
print("[Build Tools] AdvDupe2 активирован")
|
||||
end
|
||||
|
||||
if ix.config.Get("buildToolsPermaprops", true) then
|
||||
print("[Build Tools] Permaprops активирован")
|
||||
end
|
||||
end
|
||||
135
garrysmod/gamemodes/militaryrp/plugins/capture/cl_plugin.lua
Normal file
135
garrysmod/gamemodes/militaryrp/plugins/capture/cl_plugin.lua
Normal file
@@ -0,0 +1,135 @@
|
||||
PLUGIN.captureData = PLUGIN.captureData or {}
|
||||
|
||||
net.Receive("ixCaptureSync", function()
|
||||
local plugin = ix.plugin.Get("capture")
|
||||
if (!plugin) then return end
|
||||
|
||||
local point = net.ReadEntity()
|
||||
local progress = net.ReadFloat()
|
||||
local owner = net.ReadUInt(8)
|
||||
local capturing = net.ReadUInt(8)
|
||||
local players = net.ReadUInt(8)
|
||||
local blocked = net.ReadBool()
|
||||
|
||||
if (IsValid(point)) then
|
||||
plugin.captureData[point] = {
|
||||
progress = progress,
|
||||
owner = owner,
|
||||
capturing = capturing,
|
||||
players = players,
|
||||
blocked = blocked
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
local PANEL = {}
|
||||
|
||||
function PANEL:Init()
|
||||
self.progress = 0
|
||||
self.targetProgress = 0
|
||||
self.alpha = 0
|
||||
self.targetAlpha = 0
|
||||
self.blinkAlpha = 0
|
||||
end
|
||||
|
||||
function PANEL:SetCaptureData(data)
|
||||
self.targetProgress = data.progress or 0
|
||||
self.owner = data.owner or 0
|
||||
self.capturing = data.capturing or 0
|
||||
self.players = data.players or 0
|
||||
self.blocked = data.blocked or false
|
||||
|
||||
if (data.capturing and data.capturing > 0) then
|
||||
self.targetAlpha = 255
|
||||
else
|
||||
self.targetAlpha = 0
|
||||
end
|
||||
end
|
||||
|
||||
function PANEL:Think()
|
||||
self.progress = Lerp(FrameTime() * 5, self.progress, self.targetProgress)
|
||||
self.alpha = Lerp(FrameTime() * 8, self.alpha, self.targetAlpha)
|
||||
|
||||
if (self.blocked) then
|
||||
self.blinkAlpha = math.abs(math.sin(CurTime() * 5)) * 255
|
||||
else
|
||||
self.blinkAlpha = 0
|
||||
end
|
||||
|
||||
local plugin = ix.plugin.Get("capture")
|
||||
if (plugin) then
|
||||
self:SetSize(plugin.hudWidth, plugin.hudHeight)
|
||||
self:SetPos(ScrW() - plugin.hudWidth - plugin.hudPosX, plugin.hudPosY)
|
||||
end
|
||||
end
|
||||
|
||||
function PANEL:Paint(w, h)
|
||||
if (self.alpha < 1) then return end
|
||||
|
||||
surface.SetAlphaMultiplier(self.alpha / 255)
|
||||
|
||||
local barW = 40
|
||||
local barH = h
|
||||
local padding = 10
|
||||
local barX = (w - barW) / 2
|
||||
|
||||
draw.RoundedBox(8, barX, 0, barW, barH, ColorAlpha(Color(20, 20, 20), self.alpha * 0.9))
|
||||
|
||||
if (self.progress > 0) then
|
||||
local fillH = (barH - 4) * (self.progress / 100)
|
||||
local color = Color(100, 150, 255)
|
||||
|
||||
if (self.capturing and self.capturing > 0) then
|
||||
if (self.capturing == FACTION_RUSSIAN) then
|
||||
color = Color(200, 50, 50)
|
||||
elseif (self.capturing == FACTION_UKRAINE) then
|
||||
color = Color(50, 100, 200)
|
||||
end
|
||||
end
|
||||
|
||||
if (self.blocked) then
|
||||
color = ColorAlpha(color, self.blinkAlpha)
|
||||
end
|
||||
|
||||
draw.RoundedBox(6, barX + 2, 2, barW - 4, fillH, ColorAlpha(color, self.alpha))
|
||||
end
|
||||
|
||||
local text = string.format("%.0f%%", self.progress)
|
||||
if (self.players and self.players > 0) then
|
||||
text = text .. "\n(" .. self.players .. ")"
|
||||
end
|
||||
|
||||
draw.SimpleText(text, "DermaDefault", w / 2, barH + 15, ColorAlpha(color_white, self.alpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_TOP)
|
||||
|
||||
surface.SetAlphaMultiplier(1)
|
||||
end
|
||||
|
||||
vgui.Register("ixCaptureUI", PANEL, "DPanel")
|
||||
|
||||
function PLUGIN:HUDPaint()
|
||||
local ply = LocalPlayer()
|
||||
if (!IsValid(ply)) then return end
|
||||
|
||||
for point, data in pairs(self.captureData) do
|
||||
if (IsValid(point) and ply:GetPos():Distance(point:GetPos()) <= self.captureRadius) then
|
||||
if (!IsValid(self.captureUI)) then
|
||||
self.captureUI = vgui.Create("ixCaptureUI")
|
||||
end
|
||||
|
||||
self.captureUI:SetCaptureData(data)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if (IsValid(self.captureUI)) then
|
||||
self.captureUI:SetCaptureData({progress = 0, owner = 0, capturing = 0, players = 0, blocked = false})
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnReloaded()
|
||||
if (IsValid(self.captureUI)) then
|
||||
self.captureUI:Remove()
|
||||
self.captureUI = nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.PrintName = "Точка захвата"
|
||||
ENT.Category = "[FT] Захват точек"
|
||||
ENT.Spawnable = true
|
||||
ENT.AdminOnly = true
|
||||
ENT.RenderGroup = RENDERGROUP_BOTH
|
||||
ENT.bNoPersist = true
|
||||
|
||||
function ENT:SetupDataTables()
|
||||
self:NetworkVar("Float", 0, "CaptureProgress")
|
||||
self:NetworkVar("Int", 0, "OwnerFaction")
|
||||
self:NetworkVar("Int", 1, "FlagSkin")
|
||||
end
|
||||
|
||||
if SERVER then
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/flag/ua_flag_rework.mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMoveType(MOVETYPE_NONE)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
self:SetUseType(SIMPLE_USE)
|
||||
|
||||
local phys = self:GetPhysicsObject()
|
||||
if (IsValid(phys)) then
|
||||
phys:EnableMotion(false)
|
||||
end
|
||||
|
||||
-- Установка начального скина (никто не захватил)
|
||||
self:SetFlagSkin(2)
|
||||
self:SetSkin(2)
|
||||
self:SetOwnerFaction(0)
|
||||
|
||||
local plugin = ix.plugin.Get("capture")
|
||||
if (plugin) then
|
||||
plugin:RegisterCapturePoint(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- Функция для установки владельца с автоматической сменой скина
|
||||
function ENT:SetOwnerFactionID(factionID)
|
||||
self:SetOwnerFaction(factionID or 0)
|
||||
|
||||
local skinID = 2 -- По умолчанию никто
|
||||
|
||||
if factionID == FACTION_RUSSIAN then
|
||||
skinID = 1 -- РФ - скин 1
|
||||
elseif factionID == FACTION_UKRAINE then
|
||||
skinID = 0 -- ВСУ - скин 0
|
||||
end
|
||||
|
||||
self:SetFlagSkin(skinID)
|
||||
self:SetSkin(skinID)
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
end
|
||||
else
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
|
||||
-- Синхронизация скина с NetworkVar
|
||||
local skinID = self:GetFlagSkin()
|
||||
if skinID and self:GetSkin() ~= skinID then
|
||||
self:SetSkin(skinID)
|
||||
end
|
||||
|
||||
--local plugin = ix.plugin.Get("capture")
|
||||
--if (!plugin or !plugin.captureData[self]) then return end
|
||||
--
|
||||
--local data = plugin.captureData[self]
|
||||
--local pos = self:GetPos() + Vector(0, 0, plugin.labelOffsetZ)
|
||||
--local ang = EyeAngles()
|
||||
--ang:RotateAroundAxis(ang:Forward(), 90)
|
||||
--ang:RotateAroundAxis(ang:Right(), 90)
|
||||
|
||||
--cam.Start3D2D(pos, Angle(0, ang.y, 90), plugin.labelScale)
|
||||
-- if (data.capturing and data.capturing > 0) then
|
||||
-- local barW = 400
|
||||
-- local barH = 60
|
||||
-- local x, y = -barW / 2, -barH / 2
|
||||
--
|
||||
-- draw.RoundedBox(8, x, y - 80, barW, barH, Color(20, 20, 20, 230))
|
||||
--
|
||||
-- local fillW = (barW - 8) * (data.progress / 100)
|
||||
-- local color = Color(100, 150, 255)
|
||||
--
|
||||
-- if (data.capturing == FACTION_RUSSIAN) then
|
||||
-- color = Color(200, 50, 50)
|
||||
-- elseif (data.capturing == FACTION_UKRAINE) then
|
||||
-- color = Color(50, 100, 200)
|
||||
-- end
|
||||
--
|
||||
-- draw.RoundedBox(6, x + 4, y - 76, fillW, barH - 8, color)
|
||||
--
|
||||
-- local text = string.format("%.0f%%", data.progress)
|
||||
-- draw.SimpleText(text, "DermaLarge", 0, -80, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
-- end
|
||||
--
|
||||
-- local ownerText = "Никем не захвачена"
|
||||
-- local ownerColor = Color(150, 150, 150)
|
||||
--
|
||||
-- if (data.owner and data.owner > 0) then
|
||||
-- local faction = ix.faction.Get(data.owner)
|
||||
-- if (faction) then
|
||||
-- ownerText = faction.name
|
||||
-- if (data.owner == FACTION_RUSSIAN) then
|
||||
-- ownerColor = Color(200, 50, 50)
|
||||
-- elseif (data.owner == FACTION_UKRAINE) then
|
||||
-- ownerColor = Color(50, 100, 200)
|
||||
-- end
|
||||
-- end
|
||||
-- end
|
||||
--
|
||||
-- draw.SimpleText(ownerText, "DermaLarge", 0, 0, ownerColor, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
--cam.End3D2D()
|
||||
end
|
||||
end
|
||||
44
garrysmod/gamemodes/militaryrp/plugins/capture/sh_plugin.lua
Normal file
44
garrysmod/gamemodes/militaryrp/plugins/capture/sh_plugin.lua
Normal file
@@ -0,0 +1,44 @@
|
||||
PLUGIN.name = "Capture Points"
|
||||
PLUGIN.author = "RefoselTeamWork"
|
||||
PLUGIN.description = "Система захвата точек"
|
||||
|
||||
PLUGIN.captureRadius = 1024
|
||||
PLUGIN.captureBaseTime = 35
|
||||
PLUGIN.moneyReward = 3000
|
||||
PLUGIN.arsenalReward = 500
|
||||
PLUGIN.vehicleReward = 500
|
||||
|
||||
-- Настройки периодических наград
|
||||
PLUGIN.tickInterval = 60 -- Интервал начисления в секундах
|
||||
PLUGIN.tickSupplyPerPoint = 200 -- Очки снабжения за одну точку
|
||||
PLUGIN.tickVehiclePerPoint = 150 -- Очки техники за одну точку
|
||||
|
||||
PLUGIN.hudPosX = 0
|
||||
PLUGIN.hudPosY = 20
|
||||
PLUGIN.hudWidth = 200
|
||||
PLUGIN.hudHeight = 300
|
||||
|
||||
PLUGIN.labelOffsetZ = 80
|
||||
PLUGIN.labelScale = 0.1
|
||||
|
||||
PLUGIN.factionMinOnline = {
|
||||
[FACTION_RUSSIAN] = 1,
|
||||
[FACTION_UKRAINE] = 0
|
||||
}
|
||||
|
||||
PLUGIN.defaultPoints = {
|
||||
{pos = Vector(-6788.718262, -2758.777832, 620.980103), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(-386.355896, -13121.211914, 139.868973), ang = Angle(0, 90, 0)},
|
||||
|
||||
{pos = Vector(4969.474121, -10055.811523, 114.868973), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(15625.968750, -13238.951172, 282.868958), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(12809.052734, -4790.340820, 115.868973), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(13539.985352, 4858.571289, 121.868973), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(1916.585205, 2236.532715, 226.868958), ang = Angle(0, 90, 0)},
|
||||
{pos = Vector(5527.783203, 2510.990479, 736.868958), ang = Angle(0, 90, 0)},
|
||||
}
|
||||
|
||||
PLUGIN.minCapturePlayers = 1
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
414
garrysmod/gamemodes/militaryrp/plugins/capture/sv_plugin.lua
Normal file
414
garrysmod/gamemodes/militaryrp/plugins/capture/sv_plugin.lua
Normal file
@@ -0,0 +1,414 @@
|
||||
util.AddNetworkString("ixCaptureSync")
|
||||
|
||||
PLUGIN.capturePoints = PLUGIN.capturePoints or {}
|
||||
|
||||
function PLUGIN:GetPlayersInRadius(point, radius)
|
||||
local players = {}
|
||||
local pos = point:GetPos()
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
if ply:Alive() and pos:Distance(ply:GetPos()) <= radius and not (ply.IsAdminMode and ply:IsAdminMode()) then
|
||||
table.insert(players, ply)
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
function PLUGIN:GetFactionFromPlayers(players)
|
||||
local factions = {}
|
||||
|
||||
for _, ply in ipairs(players) do
|
||||
local char = ply:GetCharacter()
|
||||
if (char) then
|
||||
local faction = char:GetFaction()
|
||||
factions[faction] = (factions[faction] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
return factions
|
||||
end
|
||||
|
||||
function PLUGIN:GetFactionOnlineCount(faction)
|
||||
local count = 0
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
local char = ply:GetCharacter()
|
||||
if char and char:GetFaction() == faction and not (ply.IsAdminMode and ply:IsAdminMode()) then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
function PLUGIN:CanFactionCapture(faction)
|
||||
local minOnline = self.factionMinOnline[faction]
|
||||
if (!minOnline) then return true end
|
||||
|
||||
local online = self:GetFactionOnlineCount(faction)
|
||||
return online >= minOnline
|
||||
end
|
||||
|
||||
function PLUGIN:CanCapture(factions)
|
||||
local count = 0
|
||||
for _ in pairs(factions) do
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
return count == 1
|
||||
end
|
||||
|
||||
function PLUGIN:GiveRewards(faction, players)
|
||||
for _, ply in ipairs(players) do
|
||||
local char = ply:GetCharacter()
|
||||
if (char) then
|
||||
char:GiveMoney(self.moneyReward)
|
||||
ply:Notify("Вы получили " .. self.moneyReward .. " за захват точки!")
|
||||
end
|
||||
end
|
||||
|
||||
if (ix.plugin.list["arsenal"]) then
|
||||
ix.plugin.list["arsenal"]:AddFactionSupply(faction, self.arsenalReward)
|
||||
end
|
||||
|
||||
if (ix.plugin.list["vehicles"]) then
|
||||
ix.plugin.list["vehicles"]:AddFactionPoints(faction, self.vehicleReward)
|
||||
end
|
||||
|
||||
-- Логируем выдачу наград
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if (serverlogsPlugin) then
|
||||
local factionName = ix.faction.Get(faction).name or tostring(faction)
|
||||
local message = string.format("Фракция '%s' получила награду за захват: %d снабжения, %d очков техники, %d денег на игрока",
|
||||
factionName, self.arsenalReward, self.vehicleReward, self.moneyReward)
|
||||
serverlogsPlugin:AddLog("CAPTURE_REWARD", message, nil, {
|
||||
faction = faction,
|
||||
factionName = factionName,
|
||||
arsenalReward = self.arsenalReward,
|
||||
vehicleReward = self.vehicleReward,
|
||||
moneyReward = self.moneyReward,
|
||||
playerCount = #players
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ProcessCapture(point)
|
||||
local players = self:GetPlayersInRadius(point, self.captureRadius)
|
||||
|
||||
if (#players == 0) then
|
||||
if point.capturingFaction != nil then
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local factions = self:GetFactionFromPlayers(players)
|
||||
|
||||
if (!self:CanCapture(factions)) then
|
||||
if point.capturingFaction != nil or !point.captureBlocked then
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
point.captureBlocked = true
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
point.captureBlocked = false
|
||||
|
||||
local faction, playerCount = next(factions)
|
||||
|
||||
if (playerCount < self.minCapturePlayers) then
|
||||
if point.capturingFaction != nil or !point.captureBlocked then
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
point.captureBlocked = true
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if (!self:CanFactionCapture(faction)) then
|
||||
if point.capturingFaction != nil or !point.captureBlocked then
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
point.captureBlocked = true
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if (point.ownerFaction == faction) then
|
||||
if point.captureProgress != 100 or point.capturingFaction != nil then
|
||||
point.captureProgress = 100
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Логируем начало захвата при первом касании
|
||||
if (!point.capturingFaction or point.capturingFaction != faction) then
|
||||
-- Сбрасываем прогресс если начинается захват другой фракцией
|
||||
point.captureProgress = 0
|
||||
|
||||
-- Уведомляем владельцев о захвате
|
||||
if (point.ownerFaction and point.ownerFaction != 0 and point.ownerFaction != faction) then
|
||||
local capturerName = ix.faction.Get(faction).name or "Противник"
|
||||
for _, v in ipairs(player.GetAll()) do
|
||||
local char = v:GetCharacter()
|
||||
if (char and char:GetFaction() == point.ownerFaction) then
|
||||
v:Notify("ВНИМАНИЕ! Ваша точка захватывается фракцией " .. capturerName .. "!")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if (serverlogsPlugin) then
|
||||
local factionName = ix.faction.Get(faction).name or tostring(faction)
|
||||
local pointPos = tostring(point:GetPos())
|
||||
local message = string.format("Фракция '%s' начала захват точки (позиция: %s)", factionName, pointPos)
|
||||
serverlogsPlugin:AddLog("CAPTURE_START", message, nil, {
|
||||
faction = faction,
|
||||
factionName = factionName,
|
||||
playerCount = playerCount,
|
||||
pointPosition = pointPos
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
point.capturingFaction = faction
|
||||
point.capturingPlayers = playerCount
|
||||
|
||||
local baseSpeed = 100 / self.captureBaseTime -- Progress per second
|
||||
local captureSpeed = baseSpeed * playerCount * FrameTime()
|
||||
|
||||
point.captureProgress = (point.captureProgress or 0) + captureSpeed
|
||||
|
||||
if (point.captureProgress >= 100) then
|
||||
point.captureProgress = 100
|
||||
local oldOwner = point.ownerFaction
|
||||
point.ownerFaction = faction
|
||||
point:SetOwnerFactionID(faction)
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
|
||||
self:GiveRewards(faction, players)
|
||||
self:SaveData()
|
||||
|
||||
-- Логируем успешный захват точки
|
||||
local serverlogsPlugin = ix.plugin.list["serverlogs"]
|
||||
if (serverlogsPlugin) then
|
||||
local factionName = ix.faction.Get(faction).name or tostring(faction)
|
||||
local pointPos = tostring(point:GetPos())
|
||||
local oldFactionData = oldOwner and ix.faction.Get(oldOwner)
|
||||
local oldFactionName = (oldFactionData and oldFactionData.name) or (oldOwner and tostring(oldOwner)) or "Нейтральная"
|
||||
local message = string.format("Точка захвачена фракцией '%s' (предыдущий владелец: %s)", factionName, oldFactionName)
|
||||
serverlogsPlugin:AddLog("CAPTURE_COMPLETE", message, nil, {
|
||||
faction = faction,
|
||||
factionName = factionName,
|
||||
oldOwner = oldOwner,
|
||||
oldOwnerName = oldFactionName,
|
||||
playerCount = playerCount,
|
||||
pointPosition = pointPos
|
||||
})
|
||||
end
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
local char = ply:GetCharacter()
|
||||
if (char and char:GetFaction() == faction) then
|
||||
ply:Notify("Точка захвачена вашей фракцией!")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Only sync if progress changed significantly, something started/stopped, or a timer passed
|
||||
local curTime = CurTime()
|
||||
point.nextSync = point.nextSync or 0
|
||||
|
||||
if (point.lastSyncProgress == nil or
|
||||
math.abs(point.captureProgress - point.lastSyncProgress) >= 1 or
|
||||
curTime >= point.nextSync) then
|
||||
|
||||
self:SyncCapturePoint(point)
|
||||
point.lastSyncProgress = point.captureProgress
|
||||
point.nextSync = curTime + 0.2 -- Sync at most 5 times per second
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:SyncCapturePoint(point)
|
||||
net.Start("ixCaptureSync")
|
||||
net.WriteEntity(point)
|
||||
net.WriteFloat(point.captureProgress or 0)
|
||||
net.WriteUInt(point.ownerFaction or 0, 8)
|
||||
net.WriteUInt(point.capturingFaction or 0, 8)
|
||||
net.WriteUInt(point.capturingPlayers or 0, 8)
|
||||
net.WriteBool(point.captureBlocked or false)
|
||||
net.Broadcast()
|
||||
end
|
||||
|
||||
function PLUGIN:Think()
|
||||
if (!self.nextTickReward) then
|
||||
self.nextTickReward = CurTime() + (self.tickInterval or 300)
|
||||
elseif (CurTime() >= self.nextTickReward) then
|
||||
self:ProcessPeriodicRewards()
|
||||
self.nextTickReward = CurTime() + (self.tickInterval or 300)
|
||||
end
|
||||
|
||||
for _, point in ipairs(self.capturePoints) do
|
||||
if (IsValid(point)) then
|
||||
self:ProcessCapture(point)
|
||||
end
|
||||
end
|
||||
|
||||
if (!self.nextAutoSave or CurTime() >= self.nextAutoSave) then
|
||||
self:SaveData()
|
||||
self.nextAutoSave = CurTime() + 60
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:RegisterCapturePoint(point)
|
||||
table.insert(self.capturePoints, point)
|
||||
point.captureProgress = 0
|
||||
point.ownerFaction = nil
|
||||
point:SetOwnerFactionID(0)
|
||||
point.capturingFaction = nil
|
||||
point.capturingPlayers = 0
|
||||
end
|
||||
|
||||
function PLUGIN:EntityRemoved(entity)
|
||||
if (entity:GetClass() == "ix_capture_point") then
|
||||
for i, point in ipairs(self.capturePoints) do
|
||||
if (point == entity) then
|
||||
table.remove(self.capturePoints, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:SaveData()
|
||||
local data = {}
|
||||
|
||||
for _, point in ipairs(ents.FindByClass("ix_capture_point")) do
|
||||
if IsValid(point) then
|
||||
data[#data + 1] = {
|
||||
pos = point:GetPos(),
|
||||
angles = point:GetAngles(),
|
||||
ownerFaction = point.ownerFaction or 0,
|
||||
captureProgress = point.captureProgress or 0
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
self:SetData(data)
|
||||
end
|
||||
|
||||
function PLUGIN:LoadData()
|
||||
local data = self:GetData()
|
||||
|
||||
if (!data or #data == 0) then
|
||||
for _, v in ipairs(self.defaultPoints or {}) do
|
||||
local point = ents.Create("ix_capture_point")
|
||||
if IsValid(point) then
|
||||
point:SetPos(v.pos)
|
||||
point:SetAngles(v.ang)
|
||||
point:Spawn()
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
if IsValid(point) then
|
||||
point.ownerFaction = 0
|
||||
point:SetOwnerFactionID(0)
|
||||
point.captureProgress = 0
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
for _, v in ipairs(data) do
|
||||
local point = ents.Create("ix_capture_point")
|
||||
if IsValid(point) then
|
||||
point:SetPos(v.pos)
|
||||
point:SetAngles(v.angles)
|
||||
point:Spawn()
|
||||
|
||||
timer.Simple(0.1, function()
|
||||
if IsValid(point) then
|
||||
point.ownerFaction = v.ownerFaction
|
||||
point:SetOwnerFactionID(v.ownerFaction or 0)
|
||||
point.captureProgress = v.captureProgress or 0
|
||||
|
||||
self:SyncCapturePoint(point)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnUnloaded()
|
||||
self:SaveData()
|
||||
end
|
||||
|
||||
-- Подсчет захваченных точек каждой фракцией
|
||||
function PLUGIN:GetCapturedPointsCount()
|
||||
local counts = {}
|
||||
|
||||
for _, point in ipairs(self.capturePoints) do
|
||||
if IsValid(point) and point.ownerFaction and point.ownerFaction ~= 0 then
|
||||
counts[point.ownerFaction] = (counts[point.ownerFaction] or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
return counts
|
||||
end
|
||||
|
||||
-- Периодическое начисление очков
|
||||
function PLUGIN:ProcessPeriodicRewards()
|
||||
local counts = self:GetCapturedPointsCount()
|
||||
|
||||
for faction, pointCount in pairs(counts) do
|
||||
if pointCount > 0 then
|
||||
local supplyReward = pointCount * self.tickSupplyPerPoint
|
||||
local vehicleReward = pointCount * self.tickVehiclePerPoint
|
||||
|
||||
if ix.plugin.list["arsenal"] then
|
||||
ix.plugin.list["arsenal"]:AddFactionSupply(faction, supplyReward)
|
||||
end
|
||||
|
||||
if ix.plugin.list["vehicles"] then
|
||||
ix.plugin.list["vehicles"]:AddFactionPoints(faction, vehicleReward)
|
||||
end
|
||||
|
||||
local factionName = ix.faction.Get(faction).name or "Фракция"
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
local char = ply:GetCharacter()
|
||||
if char and char:GetFaction() == faction then
|
||||
ply:Notify(string.format("Ваша фракция получила: +%d снабжения, +%d очков техники (точек: %d)",
|
||||
supplyReward, vehicleReward, pointCount))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ix.command.Add("CaptureSystemSave", {
|
||||
description = "Сохранить точки захвата",
|
||||
superAdminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
local plugin = ix.plugin.Get("capture")
|
||||
if (!plugin) then
|
||||
return "@commandNoExist"
|
||||
end
|
||||
|
||||
plugin:SaveData()
|
||||
local count = #ents.FindByClass("ix_capture_point")
|
||||
client:Notify("Точки захвата сохранены (" .. count .. " шт.)")
|
||||
end
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
local PLUGIN = PLUGIN
|
||||
PLUGIN.name = "Carrying Entities"
|
||||
PLUGIN.author = "Scripty"
|
||||
PLUGIN.description = "Позволяет игрокам поднимать любые энтити на клавишу E через стандартную механику."
|
||||
|
||||
-- Список классов, которые ЗАПРЕЩЕНО поднимать
|
||||
local BLACKLIST = {
|
||||
["player"] = true,
|
||||
["worldspawn"] = true,
|
||||
["func_door"] = true,
|
||||
["func_door_rotating"] = true,
|
||||
["prop_door_rotating"] = true,
|
||||
["func_movelinear"] = true,
|
||||
["prop_dynamic"] = true,
|
||||
["ix_vendor"] = true,
|
||||
["gmod_hands"] = true,
|
||||
["viewmodel"] = true
|
||||
}
|
||||
|
||||
if SERVER then
|
||||
-- Этот хук вызывается ядром Helix во время GM:AllowPlayerPickup
|
||||
function PLUGIN:CanPlayerPickupEntity(client, entity)
|
||||
if (!IsValid(entity) or entity:IsPlayer() or entity:IsWorld()) then
|
||||
return false
|
||||
end
|
||||
|
||||
local class = entity:GetClass()
|
||||
if (BLACKLIST[class]) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Проверка защиты Helix (Prop Protection)
|
||||
local char = client:GetCharacter()
|
||||
local owner = entity:GetNetVar("owner", 0)
|
||||
|
||||
-- Разрешаем владельцу, админу или если объект ничей
|
||||
if (owner == 0 or (char and owner == char:GetID()) or client:IsAdmin()) then
|
||||
local phys = entity:GetPhysicsObject()
|
||||
|
||||
-- Разрешаем подбирать только то, что имеет физику
|
||||
if (IsValid(phys) and phys:IsMoveable()) then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Прямое разрешение для пакетов крови (если физика считается не-moveable)
|
||||
if (class == "bloodbag_medicmod") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
end
|
||||
1331
garrysmod/gamemodes/militaryrp/plugins/chatbox/derma/cl_chatbox.lua
Normal file
1331
garrysmod/gamemodes/militaryrp/plugins/chatbox/derma/cl_chatbox.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
local PANEL = {}
|
||||
|
||||
function PANEL:Init()
|
||||
ix.gui.chatTabCustomize = self
|
||||
|
||||
self:SetTitle(L("chatNewTab"))
|
||||
self:SetSizable(true)
|
||||
self:SetSize(ScrW() * 0.5, ScrH() * 0.5)
|
||||
|
||||
self.settings = self:Add("ixSettings")
|
||||
self.settings:Dock(FILL)
|
||||
self.settings:SetSearchEnabled(true)
|
||||
self.settings:AddCategory(L("chatAllowedClasses"))
|
||||
|
||||
-- controls
|
||||
local controlsPanel = self:Add("Panel")
|
||||
controlsPanel:Dock(BOTTOM)
|
||||
controlsPanel:DockMargin(0, 4, 0, 0)
|
||||
controlsPanel:SetTall(32)
|
||||
|
||||
self.create = controlsPanel:Add("DButton")
|
||||
self.create:SetText(L("create"))
|
||||
self.create:SizeToContents()
|
||||
self.create:Dock(FILL)
|
||||
self.create:DockMargin(0, 0, 4, 0)
|
||||
self.create.DoClick = ix.util.Bind(self, self.CreateClicked)
|
||||
|
||||
local uncheckAll = controlsPanel:Add("DButton")
|
||||
uncheckAll:SetText(L("uncheckAll"))
|
||||
uncheckAll:SizeToContents()
|
||||
uncheckAll:Dock(RIGHT)
|
||||
uncheckAll.DoClick = function()
|
||||
self:SetAllValues(false)
|
||||
end
|
||||
|
||||
local checkAll = controlsPanel:Add("DButton")
|
||||
checkAll:SetText(L("checkAll"))
|
||||
checkAll:SizeToContents()
|
||||
checkAll:Dock(RIGHT)
|
||||
checkAll:DockMargin(0, 0, 4, 0)
|
||||
checkAll.DoClick = function()
|
||||
self:SetAllValues(true)
|
||||
end
|
||||
|
||||
-- chat class settings
|
||||
self.name = self.settings:AddRow(ix.type.string)
|
||||
self.name:SetText(L("chatTabName"))
|
||||
self.name:SetValue(L("chatNewTabTitle"))
|
||||
self.name:SetZPos(-1)
|
||||
|
||||
for k, _ in SortedPairs(ix.chat.classes) do
|
||||
local panel = self.settings:AddRow(ix.type.bool, L("chatAllowedClasses"))
|
||||
panel:SetText(k)
|
||||
panel:SetValue(true, true)
|
||||
end
|
||||
|
||||
self.settings:SizeToContents()
|
||||
self:Center()
|
||||
self:MakePopup()
|
||||
end
|
||||
|
||||
function PANEL:PopulateFromTab(name, filter)
|
||||
self.tab = name
|
||||
|
||||
self:SetTitle(L("chatCustomize"))
|
||||
self.create:SetText(L("update"))
|
||||
self.name:SetValue(name)
|
||||
|
||||
for _, v in ipairs(self.settings:GetRows()) do
|
||||
if (filter[v:GetText()]) then
|
||||
v:SetValue(false, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PANEL:SetAllValues(bValue)
|
||||
for _, v in ipairs(self.settings:GetRows()) do
|
||||
if (v == self.name) then
|
||||
continue
|
||||
end
|
||||
|
||||
v:SetValue(tobool(bValue), true)
|
||||
end
|
||||
end
|
||||
|
||||
function PANEL:CreateClicked()
|
||||
local name = self.tab and self.tab or self.name:GetValue()
|
||||
|
||||
if (self.tab != self.name:GetValue() and PLUGIN:TabExists(self.name:GetValue())) then
|
||||
ix.util.Notify(L("chatTabExists"))
|
||||
return
|
||||
end
|
||||
|
||||
local filter = {}
|
||||
|
||||
for _, v in ipairs(self.settings:GetRows()) do
|
||||
-- we only want to add entries for classes we don't want shown
|
||||
if (!v:GetValue()) then
|
||||
filter[v:GetText()] = true
|
||||
end
|
||||
end
|
||||
|
||||
if (self.tab) then
|
||||
self:OnTabUpdated(name, filter, self.name:GetValue())
|
||||
else
|
||||
self:OnTabCreated(name, filter)
|
||||
end
|
||||
|
||||
self:Remove()
|
||||
end
|
||||
|
||||
function PANEL:OnTabCreated(id, filter)
|
||||
end
|
||||
|
||||
function PANEL:OnTabUpdated(id, filter, newID)
|
||||
end
|
||||
|
||||
vgui.Register("ixChatboxTabCustomize", PANEL, "DFrame")
|
||||
@@ -0,0 +1,205 @@
|
||||
-- Переопределение стилей чатбокса под общий дизайн Military RP
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
local gradient = surface.GetTextureID("vgui/gradient-u")
|
||||
local gradientUp = surface.GetTextureID("vgui/gradient-d")
|
||||
|
||||
-- Цвета в стиле HUD
|
||||
local Colors = {
|
||||
green = Color(84, 147, 90),
|
||||
dark_green = Color(52, 91, 60),
|
||||
darker_green = Color(35, 53, 29),
|
||||
gray = Color(193, 193, 193),
|
||||
gray_dark = Color(94, 94, 94),
|
||||
black = Color(0, 0, 0, 220),
|
||||
black_light = Color(0, 0, 0, 150),
|
||||
white = Color(255, 255, 255)
|
||||
}
|
||||
|
||||
-- Переопределяем стандартные функции рисования чатбокса
|
||||
local SKIN = derma.GetDefaultSkin()
|
||||
|
||||
-- Фон чатбокса
|
||||
function SKIN:PaintChatboxBackground(panel, width, height)
|
||||
-- Blur эффект
|
||||
ix.util.DrawBlur(panel, 8)
|
||||
|
||||
-- Основной фон с градиентом
|
||||
surface.SetDrawColor(Colors.black)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
if (panel:GetActive()) then
|
||||
-- Зелёный градиент сверху когда чат активен
|
||||
surface.SetDrawColor(Colors.dark_green.r, Colors.dark_green.g, Colors.dark_green.b, 120)
|
||||
surface.SetTexture(gradientUp)
|
||||
surface.DrawTexturedRect(0, panel.tabs.buttons:GetTall(), width, height * 0.25)
|
||||
end
|
||||
|
||||
-- Обводка в стиле HUD
|
||||
surface.SetDrawColor(Colors.darker_green)
|
||||
surface.DrawOutlinedRect(0, 0, width, height)
|
||||
|
||||
-- Тонкая внутренняя обводка
|
||||
surface.SetDrawColor(Colors.green.r, Colors.green.g, Colors.green.b, 60)
|
||||
surface.DrawOutlinedRect(1, 1, width - 2, height - 2)
|
||||
end
|
||||
|
||||
-- Кнопки табов
|
||||
function SKIN:PaintChatboxTabButton(panel, width, height)
|
||||
if (panel:GetActive()) then
|
||||
-- Активная вкладка - зелёный градиент
|
||||
surface.SetDrawColor(Colors.dark_green)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
surface.SetDrawColor(Colors.green.r, Colors.green.g, Colors.green.b, 100)
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(0, 0, width, height)
|
||||
else
|
||||
-- Неактивная вкладка
|
||||
surface.SetDrawColor(Colors.black_light)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
-- Индикатор непрочитанных сообщений
|
||||
if (panel:GetUnread()) then
|
||||
surface.SetDrawColor(ColorAlpha(Color(200, 200, 50), Lerp(panel.unreadAlpha, 0, 120)))
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(0, 0, width, height - 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- Разделитель вкладок
|
||||
surface.SetDrawColor(Colors.darker_green)
|
||||
surface.DrawRect(width - 1, 0, 1, height)
|
||||
end
|
||||
|
||||
-- Панель табов
|
||||
function SKIN:PaintChatboxTabs(panel, width, height, alpha)
|
||||
-- Фон панели табов
|
||||
surface.SetDrawColor(Colors.black_light)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
-- Градиент вниз
|
||||
surface.SetDrawColor(Colors.darker_green.r, Colors.darker_green.g, Colors.darker_green.b, 150)
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(0, height * 0.5, width, height * 0.5)
|
||||
|
||||
local tab = panel:GetActiveTab()
|
||||
|
||||
if (tab) then
|
||||
local button = tab:GetButton()
|
||||
local x, _ = button:GetPos()
|
||||
|
||||
-- Линия под активной вкладкой (зелёная)
|
||||
surface.SetDrawColor(Colors.green)
|
||||
surface.DrawRect(x, height - 2, button:GetWide(), 2)
|
||||
|
||||
-- Обводка панели табов
|
||||
surface.SetDrawColor(Colors.darker_green)
|
||||
surface.DrawRect(0, height - 1, x, 1) -- слева
|
||||
surface.DrawRect(x + button:GetWide(), height - 1, width - x - button:GetWide(), 1) -- справа
|
||||
end
|
||||
end
|
||||
|
||||
-- Поле ввода текста
|
||||
function SKIN:PaintChatboxEntry(panel, width, height)
|
||||
-- Фон поля ввода
|
||||
surface.SetDrawColor(Colors.darker_green.r, Colors.darker_green.g, Colors.darker_green.b, 100)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
-- Текст
|
||||
panel:DrawTextEntryText(Colors.white, Colors.green, Colors.white)
|
||||
|
||||
-- Обводка
|
||||
surface.SetDrawColor(Colors.dark_green)
|
||||
surface.DrawOutlinedRect(0, 0, width, height)
|
||||
|
||||
-- Внутренний highlight когда активно
|
||||
if panel:HasFocus() then
|
||||
surface.SetDrawColor(Colors.green.r, Colors.green.g, Colors.green.b, 80)
|
||||
surface.DrawOutlinedRect(1, 1, width - 2, height - 2)
|
||||
end
|
||||
end
|
||||
|
||||
-- Превью команды
|
||||
function SKIN:DrawChatboxPreviewBox(x, y, text, color)
|
||||
color = color or Colors.green
|
||||
|
||||
local textWidth, textHeight = surface.GetTextSize(text)
|
||||
local width, height = textWidth + 8, textHeight + 8
|
||||
|
||||
-- Фон
|
||||
surface.SetDrawColor(Colors.dark_green)
|
||||
surface.DrawRect(x, y, width, height)
|
||||
|
||||
-- Градиент
|
||||
surface.SetDrawColor(color.r, color.g, color.b, 60)
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(x, y, width, height)
|
||||
|
||||
-- Текст
|
||||
surface.SetTextColor(Colors.white)
|
||||
surface.SetTextPos(x + width * 0.5 - textWidth * 0.5, y + height * 0.5 - textHeight * 0.5)
|
||||
surface.DrawText(text)
|
||||
|
||||
-- Обводка
|
||||
surface.SetDrawColor(Colors.darker_green)
|
||||
surface.DrawOutlinedRect(x, y, width, height)
|
||||
|
||||
return width
|
||||
end
|
||||
|
||||
-- Префикс команды
|
||||
function SKIN:DrawChatboxPrefixBox(panel, width, height)
|
||||
local color = panel:GetBackgroundColor() or Colors.green
|
||||
|
||||
-- Фон
|
||||
surface.SetDrawColor(Colors.dark_green)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
-- Градиент
|
||||
surface.SetDrawColor(color.r, color.g, color.b, 80)
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(0, 0, width, height)
|
||||
|
||||
-- Обводка
|
||||
surface.SetDrawColor(Colors.darker_green)
|
||||
surface.DrawOutlinedRect(0, 0, width, height)
|
||||
end
|
||||
|
||||
-- Автодополнение команд
|
||||
function SKIN:PaintChatboxAutocompleteEntry(panel, width, height)
|
||||
-- Выделенный фон
|
||||
if (panel.highlightAlpha > 0) then
|
||||
surface.SetDrawColor(Colors.dark_green.r, Colors.dark_green.g, Colors.dark_green.b, panel.highlightAlpha * 150)
|
||||
surface.DrawRect(0, 0, width, height)
|
||||
|
||||
-- Градиент выделения
|
||||
surface.SetDrawColor(Colors.green.r, Colors.green.g, Colors.green.b, panel.highlightAlpha * 60)
|
||||
surface.SetTexture(gradient)
|
||||
surface.DrawTexturedRect(0, 0, width, height)
|
||||
end
|
||||
|
||||
-- Разделитель между командами
|
||||
surface.SetDrawColor(Colors.darker_green.r, Colors.darker_green.g, Colors.darker_green.b, 100)
|
||||
surface.DrawRect(0, height - 1, width, 1)
|
||||
end
|
||||
|
||||
hook.Add("LoadFonts", "MilitaryRPChatFonts", function()
|
||||
-- Переопределяем шрифты чата на Montserrat
|
||||
local scale = ix.option.Get("chatFontScale", 1)
|
||||
|
||||
surface.CreateFont("ixChatFont", {
|
||||
font = "Montserrat",
|
||||
size = 17 * scale,
|
||||
weight = 400,
|
||||
extended = true
|
||||
})
|
||||
|
||||
surface.CreateFont("ixChatFontItalics", {
|
||||
font = "Montserrat",
|
||||
size = 17 * scale,
|
||||
weight = 400,
|
||||
italic = true,
|
||||
extended = true
|
||||
})
|
||||
end)
|
||||
@@ -0,0 +1,25 @@
|
||||
NAME = "English"
|
||||
|
||||
LANGUAGE = {
|
||||
-- Category
|
||||
optChat = "Chat",
|
||||
|
||||
-- Settings
|
||||
optChatNotices = "Chat Notices",
|
||||
optChatNoticesDesc = "Show system notifications in chat",
|
||||
|
||||
optChatTimestamps = "Timestamps",
|
||||
optChatTimestampsDesc = "Display message send time",
|
||||
|
||||
optChatFontScale = "Font Scale",
|
||||
optChatFontScaleDesc = "Chat font scale (0.1-2)",
|
||||
|
||||
optChatOutline = "Text Outline",
|
||||
optChatOutlineDesc = "Add outline to chat text for better readability",
|
||||
|
||||
optChatTabs = "Chat Tabs",
|
||||
optChatTabsDesc = "Chat tabs configuration (JSON)",
|
||||
|
||||
optChatPosition = "Chat Position",
|
||||
optChatPositionDesc = "Chat window position and size (JSON)",
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
NAME = "Русский"
|
||||
|
||||
LANGUAGE = {
|
||||
-- Category
|
||||
optChat = "Чат",
|
||||
|
||||
-- Settings
|
||||
optChatNotices = "Уведомления в чате",
|
||||
optChatNoticesDesc = "Показывать системные уведомления в чате",
|
||||
|
||||
optChatTimestamps = "Временные метки",
|
||||
optChatTimestampsDesc = "Отображать время отправки сообщений",
|
||||
|
||||
optChatFontScale = "Размер шрифта",
|
||||
optChatFontScaleDesc = "Масштаб шрифта в чате (0.1-2)",
|
||||
|
||||
optChatOutline = "Обводка текста",
|
||||
optChatOutlineDesc = "Добавить обводку к тексту чата для лучшей читаемости",
|
||||
|
||||
optChatTabs = "Вкладки чата",
|
||||
optChatTabsDesc = "Конфигурация вкладок чата (JSON)",
|
||||
|
||||
optChatPosition = "Позиция чата",
|
||||
optChatPositionDesc = "Позиция и размер окна чата (JSON)",
|
||||
}
|
||||
188
garrysmod/gamemodes/militaryrp/plugins/chatbox/sh_plugin.lua
Normal file
188
garrysmod/gamemodes/militaryrp/plugins/chatbox/sh_plugin.lua
Normal file
@@ -0,0 +1,188 @@
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.name = "Chatbox"
|
||||
PLUGIN.author = "`impulse"
|
||||
PLUGIN.description = "Replaces the chatbox to enable customization, autocomplete, and useful info."
|
||||
|
||||
if (CLIENT) then
|
||||
ix.util.Include("derma/cl_chatskin.lua")
|
||||
|
||||
ix.chat.history = ix.chat.history or {} -- array of strings the player has entered into the chatbox
|
||||
ix.chat.currentCommand = ""
|
||||
ix.chat.currentArguments = {}
|
||||
|
||||
ix.option.Add("chatNotices", ix.type.bool, false, {
|
||||
category = "optChat",
|
||||
name = "optChatNotices",
|
||||
description = "optChatNoticesDesc"
|
||||
})
|
||||
|
||||
ix.option.Add("chatTimestamps", ix.type.bool, false, {
|
||||
category = "optChat",
|
||||
name = "optChatTimestamps",
|
||||
description = "optChatTimestampsDesc"
|
||||
})
|
||||
|
||||
ix.option.Add("chatFontScale", ix.type.number, 1, {
|
||||
category = "optChat",
|
||||
name = "optChatFontScale",
|
||||
description = "optChatFontScaleDesc",
|
||||
min = 0.1, max = 2, decimals = 2,
|
||||
OnChanged = function()
|
||||
hook.Run("LoadFonts", ix.config.Get("font"), ix.config.Get("genericFont"))
|
||||
PLUGIN:CreateChat()
|
||||
end
|
||||
})
|
||||
|
||||
ix.option.Add("chatOutline", ix.type.bool, false, {
|
||||
category = "optChat",
|
||||
name = "optChatOutline",
|
||||
description = "optChatOutlineDesc"
|
||||
})
|
||||
|
||||
-- tabs and their respective filters
|
||||
ix.option.Add("chatTabs", ix.type.string, "", {
|
||||
category = "optChat",
|
||||
name = "optChatTabs",
|
||||
description = "optChatTabsDesc",
|
||||
hidden = function()
|
||||
return true
|
||||
end
|
||||
})
|
||||
|
||||
-- chatbox size and position
|
||||
ix.option.Add("chatPosition", ix.type.string, "", {
|
||||
category = "optChat",
|
||||
name = "optChatPosition",
|
||||
description = "optChatPositionDesc",
|
||||
hidden = function()
|
||||
return true
|
||||
end
|
||||
})
|
||||
|
||||
function PLUGIN:CreateChat()
|
||||
if (IsValid(self.panel)) then
|
||||
self.panel:Remove()
|
||||
end
|
||||
|
||||
self.panel = vgui.Create("ixChatbox")
|
||||
self.panel:SetupTabs(util.JSONToTable(ix.option.Get("chatTabs", "")))
|
||||
self.panel:SetupPosition(util.JSONToTable(ix.option.Get("chatPosition", "")))
|
||||
|
||||
hook.Run("ChatboxCreated")
|
||||
end
|
||||
|
||||
function PLUGIN:TabExists(id)
|
||||
if (!IsValid(self.panel)) then
|
||||
return false
|
||||
end
|
||||
|
||||
return self.panel.tabs:GetTabs()[id] != nil
|
||||
end
|
||||
|
||||
function PLUGIN:SaveTabs()
|
||||
local tabs = {}
|
||||
|
||||
for id, panel in pairs(self.panel.tabs:GetTabs()) do
|
||||
tabs[id] = panel:GetFilter()
|
||||
end
|
||||
|
||||
ix.option.Set("chatTabs", util.TableToJSON(tabs))
|
||||
end
|
||||
|
||||
function PLUGIN:SavePosition()
|
||||
local x, y = self.panel:GetPos()
|
||||
local width, height = self.panel:GetSize()
|
||||
|
||||
ix.option.Set("chatPosition", util.TableToJSON({x, y, width, height}))
|
||||
end
|
||||
|
||||
function PLUGIN:InitPostEntity()
|
||||
self:CreateChat()
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerBindPress(client, bind, pressed)
|
||||
bind = bind:lower()
|
||||
|
||||
if (bind:find("messagemode") and pressed) then
|
||||
-- Запрещаем открывать чат если игрок мёртв
|
||||
if !LocalPlayer():Alive() then
|
||||
return true
|
||||
end
|
||||
|
||||
self.panel:SetActive(true)
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnPauseMenuShow()
|
||||
if (!IsValid(ix.gui.chat) or !ix.gui.chat:GetActive()) then
|
||||
return
|
||||
end
|
||||
|
||||
ix.gui.chat:SetActive(false)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function PLUGIN:HUDShouldDraw(element)
|
||||
if (element == "CHudChat") then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ScreenResolutionChanged(oldWidth, oldHeight)
|
||||
self:CreateChat()
|
||||
end
|
||||
|
||||
function PLUGIN:ChatText(index, name, text, messageType)
|
||||
if (messageType == "none" and IsValid(self.panel)) then
|
||||
self.panel:AddMessage(text)
|
||||
end
|
||||
end
|
||||
|
||||
-- luacheck: globals chat
|
||||
chat.ixAddText = chat.ixAddText or chat.AddText
|
||||
|
||||
function chat.AddText(...)
|
||||
if (IsValid(PLUGIN.panel)) then
|
||||
PLUGIN.panel:AddMessage(...)
|
||||
end
|
||||
|
||||
-- log chat message to console
|
||||
local text = {}
|
||||
|
||||
for _, v in ipairs({...}) do
|
||||
if (istable(v) or isstring(v)) then
|
||||
text[#text + 1] = v
|
||||
elseif (isentity(v) and v:IsPlayer()) then
|
||||
text[#text + 1] = team.GetColor(v:Team())
|
||||
text[#text + 1] = v:Name()
|
||||
elseif (type(v) != "IMaterial") then
|
||||
text[#text + 1] = tostring(v)
|
||||
end
|
||||
end
|
||||
|
||||
text[#text + 1] = "\n"
|
||||
MsgC(unpack(text))
|
||||
end
|
||||
else
|
||||
util.AddNetworkString("ixChatMessage")
|
||||
|
||||
net.Receive("ixChatMessage", function(length, client)
|
||||
local text = net.ReadString()
|
||||
|
||||
if ((client.ixNextChat or 0) < CurTime() and isstring(text) and text:find("%S")) then
|
||||
local maxLength = ix.config.Get("chatMax")
|
||||
|
||||
if (text:utf8len() > maxLength) then
|
||||
text = text:utf8sub(0, maxLength)
|
||||
end
|
||||
|
||||
hook.Run("PlayerSay", client, text)
|
||||
client.ixNextChat = CurTime() + 0.5
|
||||
end
|
||||
end)
|
||||
end
|
||||
158
garrysmod/gamemodes/militaryrp/plugins/chatcontrol/sh_plugin.lua
Normal file
158
garrysmod/gamemodes/militaryrp/plugins/chatcontrol/sh_plugin.lua
Normal file
@@ -0,0 +1,158 @@
|
||||
PLUGIN.name = "Chat Control"
|
||||
PLUGIN.author = "RefoselDev"
|
||||
PLUGIN.description = "Удаление ненужных типов чатов"
|
||||
|
||||
|
||||
hook.Add("InitializedChatClasses", "ixChatControl.RemoveChats", function()
|
||||
ix.chat.classes.it = nil
|
||||
ix.chat.classes.connect = nil
|
||||
ix.chat.classes.disconnect = nil
|
||||
ix.chat.classes.event = nil
|
||||
|
||||
ix.chat.Register("rp", {
|
||||
OnChatAdd = function(self, speaker, text)
|
||||
local name = IsValid(speaker) and speaker:Name() or "Console"
|
||||
chat.AddText(Color(26, 221, 0), "[RP] ", name, ": ", text)
|
||||
end,
|
||||
prefix = {"/rp", "/RP"},
|
||||
description = "Чат РП отыгровок",
|
||||
color = Color(150, 200, 255),
|
||||
CanHear = function(self, speaker, listener)
|
||||
return listener:GetCharacter() != nil
|
||||
end,
|
||||
deadCanChat = false
|
||||
})
|
||||
|
||||
-- Чат между фракциями
|
||||
ix.chat.Register("adgl", {
|
||||
OnChatAdd = function(self, speaker, text)
|
||||
local name = IsValid(speaker) and speaker:Name() or "Console"
|
||||
local prefix = "[ФРАКЦИЯ] "
|
||||
local prefixColor = Color(255, 200, 100)
|
||||
|
||||
if (IsValid(speaker) and speaker:GetCharacter()) then
|
||||
local faction = speaker:GetCharacter():GetFaction()
|
||||
|
||||
if (faction == FACTION_RUSSIAN) then
|
||||
prefix = "[ВС РФ → ВСУ] "
|
||||
prefixColor = Color(255, 196, 0)
|
||||
elseif (faction == FACTION_UKRAINE) then
|
||||
prefix = "[ВСУ → ВС РФ] "
|
||||
prefixColor = Color(255, 196, 0)
|
||||
end
|
||||
end
|
||||
|
||||
chat.AddText(prefixColor, prefix, Color(255, 255, 255), name, ": ", text)
|
||||
end,
|
||||
prefix = {"/adgl"},
|
||||
description = "Чат между фракциями",
|
||||
color = Color(255, 200, 100),
|
||||
CanHear = function(self, speaker, listener)
|
||||
-- Слышат только игроки с персонажем и определенной фракцией
|
||||
if (!listener:GetCharacter()) then
|
||||
return false
|
||||
end
|
||||
|
||||
local listenerFaction = listener:GetCharacter():GetFaction()
|
||||
return listenerFaction == FACTION_RUSSIAN or listenerFaction == FACTION_UKRAINE
|
||||
end,
|
||||
deadCanChat = false
|
||||
})
|
||||
|
||||
-- Объявления стороны
|
||||
ix.chat.Register("ad", {
|
||||
OnChatAdd = function(self, speaker, text)
|
||||
local name = IsValid(speaker) and speaker:Name() or "Console"
|
||||
local prefix = "[ОБЪЯВЛЕНИЕ] "
|
||||
local prefixColor = Color(200, 200, 200)
|
||||
|
||||
if (IsValid(speaker) and speaker:GetCharacter()) then
|
||||
local faction = speaker:GetCharacter():GetFaction()
|
||||
|
||||
if (faction == FACTION_RUSSIAN) then
|
||||
prefix = "[ВС РФ] "
|
||||
prefixColor = Color(255, 100, 100)
|
||||
elseif (faction == FACTION_UKRAINE) then
|
||||
prefix = "[ВСУ] "
|
||||
prefixColor = Color(255, 196, 0)
|
||||
end
|
||||
end
|
||||
|
||||
chat.AddText(prefixColor, prefix, Color(255, 255, 255), name, ": ", text)
|
||||
end,
|
||||
prefix = {"/ad"},
|
||||
description = "Объявления стороны",
|
||||
color = Color(200, 200, 200),
|
||||
CanHear = function(self, speaker, listener)
|
||||
-- Слышат только игроки той же фракции
|
||||
if (!listener:GetCharacter() or !speaker:GetCharacter()) then
|
||||
return false
|
||||
end
|
||||
|
||||
local speakerFaction = speaker:GetCharacter():GetFaction()
|
||||
local listenerFaction = listener:GetCharacter():GetFaction()
|
||||
|
||||
return speakerFaction == listenerFaction
|
||||
end,
|
||||
deadCanChat = false
|
||||
})
|
||||
|
||||
-- Чат подразделения
|
||||
ix.chat.Register("podr", {
|
||||
OnChatAdd = function(self, speaker, text)
|
||||
local name = IsValid(speaker) and speaker:Name() or "Console"
|
||||
local prefix = "[ПОДРАЗДЕЛЕНИЕ] "
|
||||
local prefixColor = Color(100, 150, 255)
|
||||
|
||||
if (IsValid(speaker) and speaker:GetCharacter()) then
|
||||
local character = speaker:GetCharacter()
|
||||
local faction = character:GetFaction()
|
||||
local podrID = (character.GetPodr and character:GetPodr()) or 1
|
||||
local factionTable = ix.faction.indices[faction]
|
||||
|
||||
if (factionTable and factionTable.Podr and factionTable.Podr[podrID]) then
|
||||
local podrName = factionTable.Podr[podrID].name or "Неизвестно"
|
||||
prefix = "["..podrName.."] "
|
||||
|
||||
if (faction == FACTION_RUSSIAN) then
|
||||
prefixColor = Color(255, 196, 0)
|
||||
elseif (faction == FACTION_UKRAINE) then
|
||||
prefixColor = Color(255, 196, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
chat.AddText(prefixColor, prefix, Color(255, 255, 255), name, ": ", text)
|
||||
end,
|
||||
prefix = {"/podr", "/p"},
|
||||
description = "Чат подразделения",
|
||||
color = Color(100, 150, 255),
|
||||
CanHear = function(self, speaker, listener)
|
||||
-- Слышат только игроки той же фракции и того же подразделения
|
||||
if (!listener:GetCharacter() or !speaker:GetCharacter()) then
|
||||
return false
|
||||
end
|
||||
|
||||
local speakerChar = speaker:GetCharacter()
|
||||
local listenerChar = listener:GetCharacter()
|
||||
|
||||
local speakerFaction = speakerChar:GetFaction()
|
||||
local listenerFaction = listenerChar:GetFaction()
|
||||
|
||||
if (speakerFaction != listenerFaction) then
|
||||
return false
|
||||
end
|
||||
|
||||
local speakerPodr = (speakerChar.GetPodr and speakerChar:GetPodr()) or 1
|
||||
local listenerPodr = (listenerChar.GetPodr and listenerChar:GetPodr()) or 1
|
||||
|
||||
return speakerPodr == listenerPodr
|
||||
end,
|
||||
deadCanChat = false
|
||||
})
|
||||
|
||||
if (SERVER) then
|
||||
print("[Chat Control] Удалены чаты: it, connect, disconnect, event")
|
||||
print("[Chat Control] Добавлены чаты: /rp, /adgl, /ad, /podr")
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,209 @@
|
||||
-- Создание шрифтов
|
||||
surface.CreateFont("ixDeathScreenMain", {
|
||||
font = "OperiusRegular",
|
||||
size = 180,
|
||||
weight = 300,
|
||||
extended = true
|
||||
})
|
||||
|
||||
surface.CreateFont("ixDeathScreenSub", {
|
||||
font = "OperiusRegular",
|
||||
size = 90,
|
||||
weight = 300,
|
||||
extended = true
|
||||
})
|
||||
|
||||
-- Резервный шрифт если OperiusRegular недоступен
|
||||
surface.CreateFont("ixDeathScreenMainFallback", {
|
||||
font = "Roboto",
|
||||
size = 180,
|
||||
weight = 700,
|
||||
extended = true
|
||||
})
|
||||
|
||||
surface.CreateFont("ixDeathScreenSubFallback", {
|
||||
font = "Roboto",
|
||||
size = 90,
|
||||
weight = 500,
|
||||
extended = true
|
||||
})
|
||||
|
||||
surface.CreateFont("ixDeathScreenSmall", {
|
||||
font = "Roboto",
|
||||
size = 40,
|
||||
weight = 400,
|
||||
extended = true
|
||||
})
|
||||
|
||||
-- Проверка доступности шрифта
|
||||
local function GetFont(primary, fallback)
|
||||
local w, h = surface.GetTextSize("TEST")
|
||||
if w > 0 then
|
||||
return primary
|
||||
end
|
||||
return fallback
|
||||
end
|
||||
|
||||
local deathScreenPanel = nil
|
||||
|
||||
-- Получение экрана смерти от сервера
|
||||
net.Receive("ixDeathScreen", function()
|
||||
if not ix.config.Get("deathScreenEnabled") then
|
||||
return
|
||||
end
|
||||
|
||||
-- Закрываем старую панель если существует
|
||||
if IsValid(deathScreenPanel) then
|
||||
deathScreenPanel:Remove()
|
||||
end
|
||||
|
||||
local duration = net.ReadFloat()
|
||||
local minDuration = net.ReadFloat()
|
||||
local killerName = net.ReadString()
|
||||
|
||||
deathScreenPanel = vgui.Create("DFrame")
|
||||
deathScreenPanel:SetSize(ScrW(), ScrH())
|
||||
deathScreenPanel:SetPos(0, 0)
|
||||
deathScreenPanel:SetTitle("")
|
||||
deathScreenPanel:ShowCloseButton(false)
|
||||
deathScreenPanel:SetDraggable(false)
|
||||
deathScreenPanel:MakePopup()
|
||||
deathScreenPanel:SetKeyboardInputEnabled(false)
|
||||
deathScreenPanel:SetMouseInputEnabled(false)
|
||||
|
||||
local colorAlpha = 0
|
||||
local textAlpha = 0
|
||||
local fadeOutStarted = false
|
||||
local startTime = RealTime()
|
||||
local canRespawnTime = startTime + minDuration
|
||||
|
||||
local fadeInSpeed = ix.config.Get("deathScreenFadeInSpeed", 5)
|
||||
local textFadeSpeed = ix.config.Get("deathScreenTextFadeSpeed", 0.5)
|
||||
local textColor = ix.config.Get("deathScreenTextColor", Color(132, 43, 60))
|
||||
local mainText = ix.config.Get("deathScreenMainText", "YOU'RE DEAD")
|
||||
local respawnText = ix.config.Get("deathScreenRespawnText", "НАЖМИТЕ [ПРОБЕЛ] ЧТОБЫ ВОЗРОДИТЬСЯ")
|
||||
local subText = "[Убил: " .. killerName .. "]"
|
||||
|
||||
-- Убраны таймеры автоматического закрытия
|
||||
|
||||
-- Обработка нажатия пробела
|
||||
deathScreenPanel.Think = function(s)
|
||||
if (fadeOutStarted) then return end
|
||||
|
||||
local lp = LocalPlayer()
|
||||
if (!IsValid(lp)) then return end
|
||||
|
||||
-- Если игрока возродили (медик или админ), убираем экран
|
||||
-- Проверяем только спустя 2 секунды, чтобы избежать багов при переходе в режим смерти
|
||||
if (RealTime() > startTime + 2) then
|
||||
if (lp:Alive() and !lp:GetNWBool("HeartAttack", false)) then
|
||||
fadeOutStarted = true
|
||||
timer.Simple(0.5, function()
|
||||
if (IsValid(s)) then s:Remove() end
|
||||
end)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Проверка возможности сдаться
|
||||
local character = lp:GetCharacter()
|
||||
local minBalance = ix.config.Get("deathRespawnMinBalance", 5000)
|
||||
local canAfford = character and character:GetMoney() >= minBalance
|
||||
|
||||
if (canAfford and RealTime() >= canRespawnTime and input.IsKeyDown(KEY_SPACE)) then
|
||||
net.Start("ixPlayerRespawn")
|
||||
net.SendToServer()
|
||||
|
||||
fadeOutStarted = true
|
||||
timer.Simple(0.5, function()
|
||||
if (IsValid(s)) then s:Remove() end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
deathScreenPanel.Paint = function(s, w, h)
|
||||
if fadeOutStarted then
|
||||
colorAlpha = math.Approach(colorAlpha, 0, FrameTime() * 255)
|
||||
else
|
||||
colorAlpha = math.Approach(colorAlpha, 255, FrameTime() * fadeInSpeed * 255)
|
||||
end
|
||||
|
||||
draw.RoundedBox(0, 0, 0, w, h, Color(0, 0, 0, colorAlpha))
|
||||
end
|
||||
|
||||
local textLabel = vgui.Create("DLabel", deathScreenPanel)
|
||||
textLabel:SetSize(deathScreenPanel:GetWide(), deathScreenPanel:GetTall())
|
||||
textLabel:SetPos(0, 0)
|
||||
textLabel:SetText("")
|
||||
|
||||
textLabel.Paint = function(s, w, h)
|
||||
local elapsed = RealTime() - startTime
|
||||
|
||||
if fadeOutStarted then
|
||||
textAlpha = math.Approach(textAlpha, 0, FrameTime() * 255 * 4)
|
||||
elseif elapsed > 1 then
|
||||
textAlpha = math.Approach(textAlpha, 255, FrameTime() * textFadeSpeed * 255)
|
||||
end
|
||||
|
||||
-- Определяем доступные шрифты
|
||||
local mainFont = GetFont("ixDeathScreenMain", "ixDeathScreenMainFallback")
|
||||
local subFont = GetFont("ixDeathScreenSub", "ixDeathScreenSubFallback")
|
||||
|
||||
-- Рисуем текст
|
||||
draw.SimpleText(mainText, mainFont, w/2, h/2,
|
||||
Color(textColor.r, textColor.g, textColor.b, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
draw.SimpleText(subText, subFont, w/2, h/2 + 144,
|
||||
Color(textColor.r, textColor.g, textColor.b, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
|
||||
-- Рисуем оставшееся время или подсказку
|
||||
local character = LocalPlayer():GetCharacter()
|
||||
local minBalance = ix.config.Get("deathRespawnMinBalance", 5000)
|
||||
local canAfford = character and character:GetMoney() >= minBalance
|
||||
|
||||
if (RealTime() < canRespawnTime) then
|
||||
local remaining = math.max(0, math.ceil(canRespawnTime - RealTime()))
|
||||
draw.SimpleText("ВОЗРОЖДЕНИЕ ЧЕРЕЗ: " .. remaining, subFont, w/2, h/2 + 250,
|
||||
Color(textColor.r, textColor.g, textColor.b, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
elseif (canAfford) then
|
||||
draw.SimpleText(respawnText, subFont, w/2, h/2 + 250,
|
||||
Color(textColor.r, textColor.g, textColor.b, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
else
|
||||
draw.SimpleText("НЕДОСТАТОЧНО СРЕДСТВ ДЛЯ СДАЧИ (" .. ix.currency.Get(minBalance) .. ")", subFont, w/2, h/2 + 250,
|
||||
Color(200, 50, 50, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
|
||||
-- Рисуем предупреждение о штрафе
|
||||
local penalty = ix.config.Get("deathPenaltyAmount", 3000)
|
||||
if (penalty > 0) then
|
||||
local warningText = string.format(ix.config.Get("deathPenaltyWarningText", "При сдаче с вас спишут штраф: %s"), ix.currency.Get(penalty))
|
||||
draw.SimpleText(warningText, "ixDeathScreenSmall", w/2, h/2 + 330,
|
||||
Color(textColor.r, textColor.g, textColor.b, textAlpha * 0.8), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Очистка при отключении
|
||||
hook.Add("InitPostEntity", "ixDeathScreenCleanup", function()
|
||||
if IsValid(deathScreenPanel) then
|
||||
deathScreenPanel:Remove()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Камера следует за регдоллом при смерти
|
||||
hook.Add("CalcView", "ixDeathScreenView", function(ply, pos, angles, fov)
|
||||
if (IsValid(deathScreenPanel) and !ply:Alive()) then
|
||||
local ragdoll = ply:GetRagdollEntity()
|
||||
if (IsValid(ragdoll)) then
|
||||
local eyes = ragdoll:GetAttachment(ragdoll:LookupAttachment("eyes"))
|
||||
if (eyes) then
|
||||
return {
|
||||
origin = eyes.Pos,
|
||||
angles = eyes.Ang,
|
||||
fov = fov
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,138 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.name = "Murder Drones Death Screen"
|
||||
PLUGIN.description = "Добавляет кинематографичный экран смерти в стиле Murder Drones"
|
||||
PLUGIN.author = "Scripty & Ported to Helix"
|
||||
|
||||
-- Конфигурация
|
||||
ix.config.Add("deathScreenEnabled", true, "Включить экран смерти Murder Drones", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenDuration", 300, "Длительность показа экрана смерти (секунды)", nil, {
|
||||
data = {min = 1, max = 600, decimals = 1},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenMinDuration", 20, "Минимальное время до возможности возродиться (секунды)", nil, {
|
||||
data = {min = 0, max = 60, decimals = 1},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenFadeInSpeed", 5, "Скорость появления экрана", nil, {
|
||||
data = {min = 1, max = 20, decimals = 1},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenTextFadeSpeed", 0.5, "Скорость появления текста", nil, {
|
||||
data = {min = 0.1, max = 5, decimals = 1},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenTextColor", Color(132, 43, 60), "Цвет текста экрана смерти", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenMainText", "YOU'RE DEAD", "Основной текст экрана смерти", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenSubText", "[IDIOT]", "Подзаголовок экрана смерти", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenRespawnText", "НАЖМИТЕ [ПРОБЕЛ] ЧТОБЫ ВОЗРОДИТЬСЯ", "Текст подсказки для возрождения", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathScreenDisableSound", true, "Отключить стандартный звук смерти", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathPenaltyAmount", 3000, "Сумма штрафа за возрождение", nil, {
|
||||
data = {min = 0, max = 100000},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathPenaltyWarningText", "При сдаче с вас спишут штраф: %s", "Текст предупреждения о штрафе", nil, {
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
ix.config.Add("deathRespawnMinBalance", 5000, "Минимальный баланс для возможности сдаться", nil, {
|
||||
data = {min = 0, max = 100000},
|
||||
category = "Death Screen"
|
||||
})
|
||||
|
||||
if SERVER then
|
||||
util.AddNetworkString("ixDeathScreen")
|
||||
util.AddNetworkString("ixPlayerRespawn")
|
||||
|
||||
function PLUGIN:PlayerDeath(ply, inflictor, attacker)
|
||||
if not ix.config.Get("deathScreenEnabled") then
|
||||
return
|
||||
end
|
||||
|
||||
local maxDuration = ix.config.Get("deathScreenDuration", 300)
|
||||
local minDuration = ix.config.Get("deathScreenMinDuration", 20)
|
||||
local killerName = "Неизвестно"
|
||||
|
||||
if (IsValid(attacker)) then
|
||||
if (attacker:IsPlayer()) then
|
||||
killerName = attacker:Name()
|
||||
elseif (attacker:IsNPC()) then
|
||||
killerName = attacker:GetClass()
|
||||
elseif (IsValid(attacker:GetOwner()) and attacker:GetOwner():IsPlayer()) then
|
||||
killerName = attacker:GetOwner():Name()
|
||||
elseif (attacker.GetCreator and IsValid(attacker:GetCreator()) and attacker:GetCreator():IsPlayer()) then
|
||||
killerName = attacker:GetCreator():Name()
|
||||
else
|
||||
killerName = (attacker:GetClass() != "worldspawn") and attacker:GetClass() or "Окружение"
|
||||
end
|
||||
end
|
||||
|
||||
net.Start("ixDeathScreen")
|
||||
net.WriteFloat(maxDuration)
|
||||
net.WriteFloat(minDuration)
|
||||
net.WriteString(killerName)
|
||||
net.Send(ply)
|
||||
|
||||
-- Блокируем стандартный спавн Helix
|
||||
local duration = RespawnTime[ply:GetUserGroup()] or 60
|
||||
ply.ixDeathTime = RealTime()
|
||||
ply.ixNextSpawn = RealTime() + duration
|
||||
ply:SetNetVar("deathTime", CurTime() + maxDuration)
|
||||
|
||||
-- Сохраняем реальное минимальное время для проверки
|
||||
ply.ixDeathScreenRespawnTime = CurTime() + minDuration
|
||||
end
|
||||
|
||||
-- Разрешаем создание стандартного регдолла Helix
|
||||
function PLUGIN:ShouldSpawnClientRagdoll(ply)
|
||||
return true
|
||||
end
|
||||
|
||||
net.Receive("ixPlayerRespawn", function(len, ply)
|
||||
if (IsValid(ply) and !ply:Alive() and ply:GetCharacter()) then
|
||||
if ((ply.ixDeathScreenRespawnTime or 0) <= CurTime()) then
|
||||
local character = ply:GetCharacter()
|
||||
local minBalance = ix.config.Get("deathRespawnMinBalance", 5000)
|
||||
|
||||
-- Проверка баланса перед спавном
|
||||
if (character:GetMoney() < minBalance) then
|
||||
ply:Notify("У вас недостаточно средств, чтобы сдаться. Нужно минимум " .. ix.currency.Get(minBalance))
|
||||
return
|
||||
end
|
||||
|
||||
local penalty = ix.config.Get("deathPenaltyAmount", 3000)
|
||||
if (penalty > 0) then
|
||||
character:TakeMoney(penalty)
|
||||
ply:Notify("Вы сдались. С вашего счета списано " .. ix.currency.Get(penalty) .. " штрафа.")
|
||||
end
|
||||
|
||||
ply:Spawn()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
ix.util.Include("cl_deathscreen.lua")
|
||||
@@ -0,0 +1,11 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
-- Просмотр украденного документа
|
||||
net.Receive("ixDisguise_ViewStolenID", function()
|
||||
local data = net.ReadTable()
|
||||
|
||||
local plugin = ix.plugin.Get("military_id")
|
||||
if plugin then
|
||||
plugin:ShowDocument(data)
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,83 @@
|
||||
ITEM.name = "Украденный военный билет"
|
||||
ITEM.description = "Военный билет, снятый с убитого противника. Содержит его данные."
|
||||
ITEM.model = "models/props_c17/paper01.mdl"
|
||||
ITEM.width = 1
|
||||
ITEM.height = 1
|
||||
ITEM.category = "Маскировка"
|
||||
ITEM.noDrop = false
|
||||
|
||||
-- Функция предъявления документа
|
||||
ITEM.functions.Show = {
|
||||
name = "Предъявить",
|
||||
tip = "Показать военный билет ближайшему игроку",
|
||||
icon = "icon16/eye.png",
|
||||
OnRun = function(item)
|
||||
local client = item.player
|
||||
|
||||
if CLIENT then
|
||||
-- Создаем меню выбора игрока
|
||||
local menu = DermaMenu()
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
if ply ~= client and ply:GetPos():Distance(client:GetPos()) <= 200 then
|
||||
menu:AddOption(ply:Name(), function()
|
||||
net.Start("ixDisguise_ShowStolenID")
|
||||
net.WriteUInt(ply:UserID(), 16)
|
||||
net.WriteUInt(item:GetID(), 32)
|
||||
net.SendToServer()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if menu:ChildCount() == 0 then
|
||||
menu:AddOption("Нет игроков поблизости", function() end):SetDisabled(true)
|
||||
end
|
||||
|
||||
menu:Open()
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
OnCanRun = function(item)
|
||||
return !IsValid(item.entity)
|
||||
end
|
||||
}
|
||||
|
||||
-- Функция просмотра своего документа
|
||||
ITEM.functions.View = {
|
||||
name = "Просмотреть",
|
||||
tip = "Посмотреть содержимое военного билета",
|
||||
icon = "icon16/page_white_text.png",
|
||||
OnRun = function(item)
|
||||
local client = item.player
|
||||
|
||||
if SERVER then
|
||||
local data = {
|
||||
name = item:GetData("name", "Неизвестно"),
|
||||
faction = item:GetData("faction", "Неизвестно"),
|
||||
subdivision = item:GetData("subdivision", "Неизвестно"),
|
||||
specialization = item:GetData("specialization", "Неизвестно"),
|
||||
rank = item:GetData("rank", "Неизвестно")
|
||||
}
|
||||
|
||||
net.Start("ixDisguise_ViewStolenID")
|
||||
net.WriteTable(data)
|
||||
net.Send(client)
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
OnCanRun = function(item)
|
||||
return !IsValid(item.entity)
|
||||
end
|
||||
}
|
||||
|
||||
-- Кастомное отображение в инвентаре
|
||||
if CLIENT then
|
||||
function ITEM:PaintOver(item, w, h)
|
||||
local victimName = self:GetData("name")
|
||||
if victimName then
|
||||
draw.SimpleText(victimName, "DermaDefault", w/2, h - 5, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,101 @@
|
||||
ITEM.name = "Украденная форма"
|
||||
ITEM.description = "Военная форма, снятая с убитого противника. Позволяет замаскироваться под врага."
|
||||
ITEM.model = "models/props_c17/suitcase001a.mdl"
|
||||
ITEM.width = 2
|
||||
ITEM.height = 2
|
||||
ITEM.category = "Маскировка"
|
||||
ITEM.noDrop = false
|
||||
|
||||
-- Функция надевания формы
|
||||
ITEM.functions.Wear = {
|
||||
name = "Надеть",
|
||||
tip = "Надеть украденную форму для маскировки",
|
||||
icon = "icon16/user_suit.png",
|
||||
OnRun = function(item)
|
||||
local client = item.player
|
||||
local character = client:GetCharacter()
|
||||
|
||||
if SERVER then
|
||||
-- Проверяем кулдаун
|
||||
local plugin = ix.plugin.Get("disguise")
|
||||
local canUse, msg = plugin:CanUseDisguise(client)
|
||||
|
||||
if not canUse then
|
||||
client:Notify(msg)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Проверяем, не в маскировке ли уже
|
||||
if character:GetData("disguised") then
|
||||
client:Notify("Вы уже в маскировке!")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Получаем украденную модель
|
||||
local stolenModel = item:GetData("stolenModel")
|
||||
if not stolenModel then
|
||||
client:Notify("Форма повреждена!")
|
||||
return false
|
||||
end
|
||||
|
||||
-- Сохраняем оригинальную модель и надеваем украденную
|
||||
local originalModel = client:GetModel()
|
||||
character:SetData("originalModel", originalModel)
|
||||
character:SetData("disguiseModel", stolenModel)
|
||||
character:SetData("disguised", true)
|
||||
|
||||
client:SetModel(stolenModel)
|
||||
|
||||
local victimName = item:GetData("victimName", "неизвестного")
|
||||
client:Notify("Вы надели форму " .. victimName .. ". Будьте осторожны!")
|
||||
|
||||
-- Удаляем предмет после использования
|
||||
item:Remove()
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
OnCanRun = function(item)
|
||||
return !IsValid(item.entity)
|
||||
end
|
||||
}
|
||||
|
||||
-- Функция снятия формы
|
||||
ITEM.functions.Remove = {
|
||||
name = "Снять",
|
||||
tip = "Снять маскировку",
|
||||
icon = "icon16/user_delete.png",
|
||||
OnRun = function(item)
|
||||
local client = item.player
|
||||
local character = client:GetCharacter()
|
||||
|
||||
if SERVER then
|
||||
if not character:GetData("disguised") then
|
||||
client:Notify("Вы не в маскировке!")
|
||||
return false
|
||||
end
|
||||
|
||||
local plugin = ix.plugin.Get("disguise")
|
||||
if plugin then
|
||||
plugin:RemoveDisguise(client)
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
OnCanRun = function(item)
|
||||
local client = item.player
|
||||
local character = client and client:GetCharacter()
|
||||
return !IsValid(item.entity) and character and character:GetData("disguised")
|
||||
end
|
||||
}
|
||||
|
||||
-- Кастомное отображение в инвентаре
|
||||
if CLIENT then
|
||||
function ITEM:PaintOver(item, w, h)
|
||||
local victimName = self:GetData("victimName")
|
||||
if victimName then
|
||||
draw.SimpleText("Форма: " .. victimName, "DermaDefault", w/2, h - 5, color_white, TEXT_ALIGN_CENTER, TEXT_ALIGN_BOTTOM)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
PLUGIN.name = "Disguise System"
|
||||
PLUGIN.author = "Your Name"
|
||||
PLUGIN.description = "Система маскировки с украденной формой и документами"
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
|
||||
if SERVER then
|
||||
util.AddNetworkString("ixDisguise_ViewStolenID")
|
||||
util.AddNetworkString("ixDisguise_ShowStolenID")
|
||||
end
|
||||
|
||||
-- Конфигурация
|
||||
PLUGIN.dropChance = 0.3 -- 30% шанс выпадения предметов
|
||||
PLUGIN.disguiseCooldown = 600 -- 10 минут в секундах
|
||||
@@ -0,0 +1,99 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
-- Таблица для хранения кулдаунов
|
||||
PLUGIN.disguiseCooldowns = PLUGIN.disguiseCooldowns or {}
|
||||
|
||||
-- Снятие маскировки
|
||||
function PLUGIN:RemoveDisguise(client)
|
||||
if not IsValid(client) then return end
|
||||
|
||||
local character = client:GetCharacter()
|
||||
if not character then return end
|
||||
|
||||
-- Возвращаем оригинальную модель
|
||||
local originalModel = character:GetModel()
|
||||
client:SetModel(originalModel)
|
||||
|
||||
-- Удаляем флаг маскировки
|
||||
character:SetData("disguised", false)
|
||||
character:SetData("disguiseModel", nil)
|
||||
|
||||
-- Устанавливаем кулдаун
|
||||
self.disguiseCooldowns[client:SteamID64()] = CurTime() + self.disguiseCooldown
|
||||
|
||||
client:Notify("Маскировка снята! Вы не сможете переодеться в течение 10 минут.")
|
||||
end
|
||||
|
||||
-- Проверка кулдауна
|
||||
function PLUGIN:CanUseDisguise(client)
|
||||
local steamID = client:SteamID64()
|
||||
local cooldown = self.disguiseCooldowns[steamID]
|
||||
|
||||
if cooldown and CurTime() < cooldown then
|
||||
local remaining = math.ceil(cooldown - CurTime())
|
||||
local minutes = math.floor(remaining / 60)
|
||||
local seconds = remaining % 60
|
||||
return false, string.format("Переодеться можно через %d:%02d", minutes, seconds)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Хук на получение урона
|
||||
function PLUGIN:EntityTakeDamage(target, dmg)
|
||||
if not IsValid(target) or not target:IsPlayer() then return end
|
||||
|
||||
local character = target:GetCharacter()
|
||||
if not character then return end
|
||||
|
||||
-- Проверяем, в маскировке ли игрок
|
||||
if character:GetData("disguised") then
|
||||
self:RemoveDisguise(target)
|
||||
end
|
||||
end
|
||||
|
||||
-- Хук на взятие оружия в руки
|
||||
function PLUGIN:PlayerSwitchWeapon(client, oldWeapon, newWeapon)
|
||||
if not IsValid(client) or not IsValid(newWeapon) then return end
|
||||
|
||||
local character = client:GetCharacter()
|
||||
if not character then return end
|
||||
|
||||
-- Проверяем, в маскировке ли игрок
|
||||
if character:GetData("disguised") then
|
||||
self:RemoveDisguise(client)
|
||||
end
|
||||
end
|
||||
|
||||
-- Отправка данных украденного военного билета
|
||||
net.Receive("ixDisguise_ShowStolenID", function(len, client)
|
||||
local targetID = net.ReadUInt(16)
|
||||
local itemID = net.ReadUInt(32)
|
||||
|
||||
local target = Player(targetID)
|
||||
if not IsValid(target) or target:GetPos():Distance(client:GetPos()) > 200 then
|
||||
client:Notify("Игрок слишком далеко!")
|
||||
return
|
||||
end
|
||||
|
||||
local item = ix.item.instances[itemID]
|
||||
if not item or item.player ~= client then
|
||||
return
|
||||
end
|
||||
|
||||
-- Отправляем данные документа целевому игроку
|
||||
local data = {
|
||||
name = item:GetData("name", "Неизвестно"),
|
||||
faction = item:GetData("faction", "Неизвестно"),
|
||||
subdivision = item:GetData("subdivision", "Неизвестно"),
|
||||
specialization = item:GetData("specialization", "Неизвестно"),
|
||||
rank = item:GetData("rank", "Неизвестно")
|
||||
}
|
||||
|
||||
net.Start("ixDisguise_ViewStolenID")
|
||||
net.WriteTable(data)
|
||||
net.Send(target)
|
||||
|
||||
client:Notify("Вы предъявили военный билет игроку " .. target:Name())
|
||||
target:Notify(client:Name() .. " предъявил вам военный билет")
|
||||
end)
|
||||
@@ -0,0 +1,3 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
-- Клиентские опции (если нужны дополнительные настройки на клиенте)
|
||||
@@ -0,0 +1,6 @@
|
||||
PLUGIN.name = "Dynamic Height"
|
||||
PLUGIN.author = "RefoselTeamWork"
|
||||
PLUGIN.description = "Автоматическая настройка высоты камеры игрока в зависимости от модели персонажа."
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
@@ -0,0 +1,113 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
-- Конфигурационные переменные
|
||||
ix.config.Add("dynamicHeightEnabled", true, "Включить динамическую высоту камеры.", nil, {
|
||||
category = "Динамическая высота"
|
||||
})
|
||||
|
||||
ix.config.Add("dynamicHeightMaxManual", false, "Использовать ручную максимальную высоту.", nil, {
|
||||
category = "Динамическая высота"
|
||||
})
|
||||
|
||||
ix.config.Add("dynamicHeightMinManual", false, "Использовать ручную минимальную высоту.", nil, {
|
||||
category = "Динамическая высота"
|
||||
})
|
||||
|
||||
ix.config.Add("dynamicHeightMin", 16, "Минимальная высота камеры (присед).", nil, {
|
||||
data = {min = 5, max = 180},
|
||||
category = "Динамическая высота"
|
||||
})
|
||||
|
||||
ix.config.Add("dynamicHeightMax", 64, "Максимальная высота камеры (стоя).", nil, {
|
||||
data = {min = 5, max = 180},
|
||||
category = "Динамическая высота"
|
||||
})
|
||||
|
||||
local function UpdateView(ply)
|
||||
if not ix.config.Get("dynamicHeightEnabled", true) then
|
||||
if ply.ixDynamicHeightChanged then
|
||||
ply:SetViewOffset(Vector(0, 0, 64))
|
||||
ply:SetViewOffsetDucked(Vector(0, 0, 28))
|
||||
ply.ixDynamicHeightChanged = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Определяем высоту модели
|
||||
local height_max = 64
|
||||
local height_min = 16
|
||||
|
||||
-- Создаем временную сущность для измерения высоты стоя
|
||||
local entity = ents.Create("base_anim")
|
||||
entity:SetModel(ply:GetModel())
|
||||
entity:ResetSequence(entity:LookupSequence("idle_all_01"))
|
||||
local bone = entity:LookupBone("ValveBiped.Bip01_Neck1")
|
||||
if bone then
|
||||
height_max = entity:GetBonePosition(bone).z + 5
|
||||
end
|
||||
|
||||
-- Создаем временную сущность для измерения высоты приседа
|
||||
local entity2 = ents.Create("base_anim")
|
||||
entity2:SetModel(ply:GetModel())
|
||||
entity2:ResetSequence(entity2:LookupSequence("cidle_all"))
|
||||
local bone2 = entity2:LookupBone("ValveBiped.Bip01_Neck1")
|
||||
if bone2 then
|
||||
height_min = entity2:GetBonePosition(bone2).z + 5
|
||||
end
|
||||
|
||||
-- Удаляем временные сущности
|
||||
entity:Remove()
|
||||
entity2:Remove()
|
||||
|
||||
-- Применяем настройки высоты
|
||||
local min = ix.config.Get("dynamicHeightMin", 16)
|
||||
local max = ix.config.Get("dynamicHeightMax", 64)
|
||||
|
||||
if ix.config.Get("dynamicHeightMinManual", false) then
|
||||
ply:SetViewOffsetDucked(Vector(0, 0, min))
|
||||
else
|
||||
ply:SetViewOffsetDucked(Vector(0, 0, height_min))
|
||||
end
|
||||
|
||||
if ix.config.Get("dynamicHeightMaxManual", false) then
|
||||
ply:SetViewOffset(Vector(0, 0, max))
|
||||
else
|
||||
ply:SetViewOffset(Vector(0, 0, height_max))
|
||||
end
|
||||
|
||||
ply.ixDynamicHeightChanged = true
|
||||
end
|
||||
|
||||
local function UpdateTrueModel(ply)
|
||||
if ply:GetNWString("ixDynamicHeight:TrueModel") ~= ply:GetModel() then
|
||||
ply:SetNWString("ixDynamicHeight:TrueModel", ply:GetModel())
|
||||
UpdateView(ply)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerSpawn(ply)
|
||||
UpdateTrueModel(ply)
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerTick(ply)
|
||||
UpdateTrueModel(ply)
|
||||
end
|
||||
|
||||
-- Обновление при изменении конфигов
|
||||
local function OnConfigChanged(key, oldValue, newValue)
|
||||
if string.StartWith(key, "dynamicHeight") then
|
||||
for _, ply in pairs(player.GetAll()) do
|
||||
UpdateView(ply)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:OnConfigChanged(key, oldValue, newValue)
|
||||
if key == "dynamicHeightEnabled" or
|
||||
key == "dynamicHeightMin" or
|
||||
key == "dynamicHeightMax" or
|
||||
key == "dynamicHeightMinManual" or
|
||||
key == "dynamicHeightMaxManual" then
|
||||
OnConfigChanged(key, oldValue, newValue)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
net.Receive("AnnounceMessage", function()
|
||||
local msg = net.ReadString()
|
||||
chat.AddText(Color(255, 0, 0), msg)
|
||||
end)
|
||||
|
||||
local eventText = ""
|
||||
local eventTime = 0
|
||||
|
||||
net.Receive("EventMessage", function()
|
||||
eventText = net.ReadString()
|
||||
eventTime = CurTime() + 5 -- show for 5 seconds
|
||||
end)
|
||||
|
||||
function PLUGIN:HUDPaint()
|
||||
if CurTime() < eventTime then
|
||||
surface.SetFont("ixBigFont")
|
||||
local w, h = surface.GetTextSize(eventText)
|
||||
surface.SetTextColor(255, 255, 255)
|
||||
surface.SetTextPos(ScrW() / 2 - w / 2, 50)
|
||||
surface.DrawText(eventText)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
PLUGIN.name = "Event Announce"
|
||||
PLUGIN.author = "Scripty"
|
||||
PLUGIN.description = "/event /annonce"
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
@@ -0,0 +1,21 @@
|
||||
util.AddNetworkString("EventMessage")
|
||||
util.AddNetworkString("AnnounceMessage")
|
||||
|
||||
function PLUGIN:PlayerSay(ply, text)
|
||||
local userGroup = ply:GetUserGroup() or ""
|
||||
if userGroup == "superadmin" or userGroup == "ivent" then
|
||||
if string.StartWith(text, "/event ") then
|
||||
local msg = string.sub(text, 8)
|
||||
net.Start("EventMessage")
|
||||
net.WriteString(msg)
|
||||
net.Broadcast()
|
||||
return ""
|
||||
elseif string.StartWith(text, "/annonce ") then
|
||||
local msg = string.sub(text, 10)
|
||||
net.Start("AnnounceMessage")
|
||||
net.WriteString(msg)
|
||||
net.Broadcast()
|
||||
return ""
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
local doomsday_active = false
|
||||
local doomsday_start_time = 0
|
||||
|
||||
net.Receive("doomsday_night_hud_update", function()
|
||||
doomsday_active = net.ReadBool()
|
||||
doomsday_start_time = net.ReadInt(32)
|
||||
end)
|
||||
|
||||
function PLUGIN:DoomsdayHUDPaint()
|
||||
local active = doomsday_active or GetGlobalBool("doomsday_night_active", false)
|
||||
local start_time = doomsday_start_time == 0 and GetGlobalInt("doomsday_night_start_time", 0) or doomsday_start_time
|
||||
local alive_count = GetGlobalInt("doomsday_night_alive_count", 0)
|
||||
|
||||
if (!active or start_time == 0) then return end
|
||||
|
||||
local elapsed = CurTime() - start_time
|
||||
local minutes = math.floor(elapsed / 60)
|
||||
local seconds = math.floor(elapsed % 60)
|
||||
|
||||
local x, y = ScrW() - 280, 20
|
||||
local w, h = 260, 120
|
||||
|
||||
surface.SetDrawColor(20, 20, 40, 200)
|
||||
surface.DrawRect(x, y, w, h)
|
||||
|
||||
local border_alpha = 150 + math.abs(math.sin(CurTime() * 2)) * 105
|
||||
surface.SetDrawColor(100, 0, 100, border_alpha)
|
||||
surface.DrawOutlinedRect(x, y, w, h, 3)
|
||||
|
||||
surface.SetFont("DermaDefaultBold")
|
||||
surface.SetTextColor(200, 100, 255, 255)
|
||||
local title = "🌙 СУДНАЯ НОЧЬ"
|
||||
local tw, th = surface.GetTextSize(title)
|
||||
surface.SetTextPos(x + (w - tw) / 2, y + 8)
|
||||
surface.DrawText(title)
|
||||
|
||||
surface.SetFont("DermaDefault")
|
||||
surface.SetTextColor(255, 255, 255, 255)
|
||||
local time_text = string.format("Время: %02d:%02d", minutes, seconds)
|
||||
tw, th = surface.GetTextSize(time_text)
|
||||
surface.SetTextPos(x + (w - tw) / 2, y + th + 15)
|
||||
surface.DrawText(time_text)
|
||||
|
||||
local alive_pulse = 200 + math.abs(math.sin(CurTime() * 3)) * 55
|
||||
surface.SetTextColor(255, 150, 150, alive_pulse)
|
||||
local alive_text = string.format("Выживших: %d", alive_count)
|
||||
tw, th = surface.GetTextSize(alive_text)
|
||||
surface.SetTextPos(x + (w - tw) / 2, y + th + 45)
|
||||
surface.DrawText(alive_text)
|
||||
|
||||
if (LocalPlayer():GetObserverMode() != OBS_MODE_NONE) then
|
||||
surface.SetTextColor(255, 100, 100, alive_pulse)
|
||||
local spec_text = "РЕЖИМ НАБЛЮДЕНИЯ"
|
||||
tw, th = surface.GetTextSize(spec_text)
|
||||
surface.SetTextPos(x + (w - tw) / 2, y + h - 25)
|
||||
surface.DrawText(spec_text)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:DoomsdayPlayerStats()
|
||||
if (!GetGlobalBool("doomsday_night_active", false)) then return end
|
||||
local ply = LocalPlayer()
|
||||
|
||||
local x, y, w, h = 20, 20, 220, 120
|
||||
surface.SetDrawColor(0, 0, 0, 150)
|
||||
surface.DrawRect(x, y, w, h)
|
||||
surface.SetDrawColor(100, 0, 100, 200)
|
||||
surface.DrawOutlinedRect(x, y, w, h, 2)
|
||||
|
||||
surface.SetFont("DermaDefaultBold")
|
||||
surface.SetTextColor(200, 200, 255, 255)
|
||||
surface.SetTextPos(x + 10, y + 10)
|
||||
surface.DrawText("📊 Ваша статистика:")
|
||||
|
||||
surface.SetFont("DermaDefault")
|
||||
surface.SetTextColor(255, 255, 255, 255)
|
||||
surface.SetTextPos(x + 10, y + 35)
|
||||
surface.DrawText(string.format("❤️ HP: %d", ply:Health()))
|
||||
surface.SetTextPos(x + 10, y + 55)
|
||||
surface.DrawText(string.format("🛡️ Armor: %d", ply:Armor()))
|
||||
|
||||
local weapon = ply:GetActiveWeapon()
|
||||
if (IsValid(weapon)) then
|
||||
local w_name = string.gsub(weapon:GetClass(), "tfa_ins2_", ""):upper()
|
||||
surface.SetTextPos(x + 10, y + 75)
|
||||
surface.DrawText("🔫 Оружие: " .. w_name)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:DoomsdayDeathEffect()
|
||||
if (!GetGlobalBool("doomsday_night_active", false)) then return end
|
||||
local ply = LocalPlayer()
|
||||
if (ply:Alive()) then return end
|
||||
|
||||
local alpha = 50 + math.abs(math.sin(CurTime() * 2)) * 30
|
||||
surface.SetDrawColor(150, 0, 0, alpha)
|
||||
surface.DrawRect(0, 0, ScrW(), ScrH())
|
||||
|
||||
surface.SetFont("DermaLarge")
|
||||
local title_pulse = 200 + math.abs(math.sin(CurTime() * 3)) * 55
|
||||
surface.SetTextColor(255, 50, 50, title_pulse)
|
||||
local text = "💀 ВЫБЫЛ 💀"
|
||||
local tw, th = surface.GetTextSize(text)
|
||||
surface.SetTextPos((ScrW() - tw) / 2, ScrH() / 2 - 100)
|
||||
surface.DrawText(text)
|
||||
end
|
||||
|
||||
function PLUGIN:HUDPaint()
|
||||
self:DrawEventHUD()
|
||||
self:DoomsdayHUDPaint()
|
||||
self:DoomsdayPlayerStats()
|
||||
self:DoomsdayDeathEffect()
|
||||
end
|
||||
@@ -0,0 +1,131 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
local event_active = false
|
||||
local event_start_time = 0
|
||||
local event_zone_active = false
|
||||
local event_zone_min = nil
|
||||
local event_zone_max = nil
|
||||
|
||||
-- Net Receivers
|
||||
net.Receive("events_system_notification", function()
|
||||
local message = net.ReadString()
|
||||
local lines = string.Split(message, "\n")
|
||||
for _, line in ipairs(lines) do
|
||||
if (string.Trim(line) != "") then
|
||||
chat.AddText(Color(255, 100, 100), " [EVENT] ", Color(255, 255, 255), line)
|
||||
end
|
||||
end
|
||||
surface.PlaySound("buttons/button15.wav")
|
||||
end)
|
||||
|
||||
net.Receive("events_system_hud_update", function()
|
||||
event_active = net.ReadBool()
|
||||
event_start_time = net.ReadInt(32)
|
||||
end)
|
||||
|
||||
net.Receive("events_system_zone_update", function()
|
||||
event_zone_active = net.ReadBool()
|
||||
if (event_zone_active) then
|
||||
event_zone_min = net.ReadVector()
|
||||
event_zone_max = net.ReadVector()
|
||||
else
|
||||
event_zone_min = nil
|
||||
event_zone_max = nil
|
||||
end
|
||||
end)
|
||||
|
||||
-- Fonts
|
||||
surface.CreateFont("EventHUD_Title", { font = "Roboto", size = 24, weight = 700, antialias = true, shadow = true })
|
||||
surface.CreateFont("EventHUD_Time", { font = "Roboto", size = 32, weight = 800, antialias = true, shadow = false })
|
||||
surface.CreateFont("EventHUD_Label", { font = "Roboto", size = 16, weight = 500, antialias = true })
|
||||
surface.CreateFont("EventHUD_Warning", { font = "Roboto", size = 14, weight = 600, antialias = true })
|
||||
|
||||
local function DrawGradientBox(x, y, w, h, color1, color2, vertical)
|
||||
local steps = 20
|
||||
local step_size = vertical and (h / steps) or (w / steps)
|
||||
for i = 0, steps - 1 do
|
||||
local t = i / steps
|
||||
local r = Lerp(t, color1.r, color2.r)
|
||||
local g = Lerp(t, color1.g, color2.g)
|
||||
local b = Lerp(t, color1.b, color2.b)
|
||||
local a = Lerp(t, color1.a, color2.a)
|
||||
surface.SetDrawColor(r, g, b, a)
|
||||
if (vertical) then
|
||||
surface.DrawRect(x, y + i * step_size, w, step_size + 1)
|
||||
else
|
||||
surface.DrawRect(x + i * step_size, y, step_size + 1, h)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:DrawEventHUD()
|
||||
local active = event_active or GetGlobalBool("events_system_active", false)
|
||||
local start_time = event_start_time == 0 and GetGlobalInt("events_system_start_time", 0) or event_start_time
|
||||
|
||||
if (!active or start_time == 0) then return end
|
||||
|
||||
local elapsed = CurTime() - start_time
|
||||
local minutes = math.floor(elapsed / 60)
|
||||
local seconds = math.floor(elapsed % 60)
|
||||
|
||||
local padding = 20
|
||||
local x = ScrW() - 320 - padding
|
||||
local y = padding
|
||||
local w, h = 320, 140
|
||||
|
||||
DrawGradientBox(x, y, w, h, Color(15, 15, 20, 220), Color(25, 25, 35, 200), true)
|
||||
|
||||
local pulse = math.abs(math.sin(CurTime() * 2))
|
||||
DrawGradientBox(x, y, w, 4, Color(255, 50, 80, 180 + pulse * 75), Color(255, 100, 50, 180 + pulse * 75), false)
|
||||
|
||||
surface.SetDrawColor(255, 80, 100, 100 + pulse * 80)
|
||||
for i = 1, 2 do surface.DrawOutlinedRect(x - i, y - i, w + i * 2, h + i * 2, 1) end
|
||||
|
||||
local offsetY = y + 15
|
||||
surface.SetFont("EventHUD_Title")
|
||||
surface.SetTextColor(255, 80 + pulse * 50, 80 + pulse * 50, 255)
|
||||
local title = "⚔ БОЕВОЙ ИВЕНТ"
|
||||
local tw, th = surface.GetTextSize(title)
|
||||
surface.SetTextPos(x + (w - tw) / 2, offsetY)
|
||||
surface.DrawText(title)
|
||||
|
||||
offsetY = offsetY + th + 10
|
||||
surface.SetFont("EventHUD_Time")
|
||||
surface.SetTextColor(255, 255, 255, 255)
|
||||
local time_text = string.format("%02d:%02d", minutes, seconds)
|
||||
tw, th = surface.GetTextSize(time_text)
|
||||
surface.SetTextPos(x + (w - tw) / 2, offsetY)
|
||||
surface.DrawText(time_text)
|
||||
|
||||
offsetY = offsetY + th + 10
|
||||
surface.SetFont("EventHUD_Warning")
|
||||
surface.SetTextColor(255, 220, 100, 200 + pulse * 55)
|
||||
local warning = "⚠ При смерти выбываете"
|
||||
tw, th = surface.GetTextSize(warning)
|
||||
surface.SetTextPos(x + (w - tw) / 2, offsetY)
|
||||
surface.DrawText(warning)
|
||||
end
|
||||
|
||||
function PLUGIN:PostDrawTranslucentRenderables()
|
||||
if (!event_zone_active or !event_zone_min or !event_zone_max) then return end
|
||||
|
||||
local min, max = event_zone_min, event_zone_max
|
||||
local corners = {
|
||||
Vector(min.x, min.y, min.z), Vector(max.x, min.y, min.z), Vector(max.x, max.y, min.z), Vector(min.x, max.y, min.z),
|
||||
Vector(min.x, min.y, max.z), Vector(max.x, min.y, max.z), Vector(max.x, max.y, max.z), Vector(min.x, max.y, max.z)
|
||||
}
|
||||
|
||||
local color = Color(0, 255, 100, 150)
|
||||
render.DrawLine(corners[1], corners[2], color, true)
|
||||
render.DrawLine(corners[2], corners[3], color, true)
|
||||
render.DrawLine(corners[3], corners[4], color, true)
|
||||
render.DrawLine(corners[4], corners[1], color, true)
|
||||
render.DrawLine(corners[5], corners[6], color, true)
|
||||
render.DrawLine(corners[6], corners[7], color, true)
|
||||
render.DrawLine(corners[7], corners[8], color, true)
|
||||
render.DrawLine(corners[8], corners[5], color, true)
|
||||
render.DrawLine(corners[1], corners[5], color, true)
|
||||
render.DrawLine(corners[2], corners[6], color, true)
|
||||
render.DrawLine(corners[3], corners[7], color, true)
|
||||
render.DrawLine(corners[4], corners[8], color, true)
|
||||
end
|
||||
@@ -0,0 +1,212 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.name = "Система Ивентов"
|
||||
PLUGIN.author = "Scripty & RefoselTeamWork"
|
||||
PLUGIN.description = "Продвинутая система ивентов с сужающейся зоной и кастомными спавнами."
|
||||
|
||||
ix.config.Add("eventAllowedRanks", "superadmin,curator,ivent", "Ранги, которым разрешено управление ивентами (через запятую).", nil, {
|
||||
category = "Events"
|
||||
})
|
||||
|
||||
-- Shared Config (ported from sh_config.lua)
|
||||
PLUGIN.Config = {
|
||||
sounds = {
|
||||
event_start = "ambient/alarms/klaxon1.wav",
|
||||
event_end = "ambient/levels/citadel/citadel_hit.wav",
|
||||
player_join = "buttons/button15.wav"
|
||||
}
|
||||
}
|
||||
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("sv_doomsday.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
ix.util.Include("cl_doomsday.lua")
|
||||
|
||||
-- Commands Adaptation
|
||||
ix.command.Add("EventStart", {
|
||||
description = "Запустить стандартный боевой ивент.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (PLUGIN.active) then
|
||||
return "Ивент уже активен!"
|
||||
end
|
||||
PLUGIN:StartEvent(client)
|
||||
return "Запуск ивента..."
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventStop", {
|
||||
description = "Остановить текущий активный ивент.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (!PLUGIN.active) then
|
||||
return "Нет активного ивента для остановки!"
|
||||
end
|
||||
PLUGIN:EndEvent(client)
|
||||
return "Остановка ивента..."
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventOtkat", {
|
||||
description = "Отменить активный ивент с уведомлением.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (!PLUGIN.active) then
|
||||
return "Нет активного ивента для отмены!"
|
||||
end
|
||||
PLUGIN:EndEvent(client, true)
|
||||
return "Отмена ивента..."
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventJoin", {
|
||||
description = "Присоединиться к активному ивенту.",
|
||||
OnRun = function(self, client)
|
||||
if (!PLUGIN.active) then
|
||||
return "Нет активного ивента для входа."
|
||||
end
|
||||
|
||||
if (PLUGIN.participants[client:SteamID()]) then
|
||||
return "Вы уже участвуете!"
|
||||
end
|
||||
|
||||
PLUGIN:AddParticipant(client)
|
||||
|
||||
local participants_count = table.Count(PLUGIN.participants)
|
||||
PLUGIN:BroadcastMessage(string.format("%s присоединился к ивенту! Участников: %d", client:Nick(), participants_count))
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventAddAll", {
|
||||
description = "Добавить всех игроков онлайн в ивент.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (!PLUGIN.active) then
|
||||
return "Ивент не активен!"
|
||||
end
|
||||
PLUGIN:AddAllPlayers(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventSetZone1", {
|
||||
description = "Установить первую точку зоны ивента.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:SetZonePoint1(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventSetZone2", {
|
||||
description = "Установить вторую точку зоны ивента.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:SetZonePoint2(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventClearZone", {
|
||||
description = "Очистить зону ивента.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:ClearZone(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventZoneStart", {
|
||||
description = "Начать сужение зоны ивента.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:StartZoneShrinking(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventZoneStop", {
|
||||
description = "Остановить сужение зоны ивента.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
PLUGIN:StopZoneShrinking(client)
|
||||
end
|
||||
})
|
||||
|
||||
-- Temporary Spawns Commands
|
||||
ix.command.Add("EventSpawnAdd", {
|
||||
description = "Добавить временный спавн для фракции или spec_def.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string, -- Faction Name
|
||||
bit.bor(ix.type.number, ix.type.optional) -- spec_def
|
||||
},
|
||||
OnRun = function(self, client, factionName, specDef)
|
||||
local faction = ix.faction.teams[factionName]
|
||||
if (!faction) then
|
||||
-- Try fuzzy match
|
||||
for _, v in pairs(ix.faction.indices) do
|
||||
if (ix.util.StringMatches(v.name, factionName) or ix.util.StringMatches(v.uniqueID, factionName)) then
|
||||
faction = v
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (!faction) then
|
||||
return "Некорректное название фракции!"
|
||||
end
|
||||
|
||||
PLUGIN:AddTempSpawn(faction.uniqueID, specDef, client:GetPos(), client:GetAngles())
|
||||
|
||||
local msg = "Временный спавн добавлен для фракции: " .. faction.name
|
||||
if (specDef) then
|
||||
msg = msg .. " (spec_def: " .. specDef .. ")"
|
||||
end
|
||||
return msg
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("EventSpawnRemove", {
|
||||
description = "Удалить временные спавны ивента в радиусе.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
bit.bor(ix.type.number, ix.type.optional) -- Radius
|
||||
},
|
||||
OnRun = function(self, client, radius)
|
||||
radius = radius or 120
|
||||
local count = PLUGIN:RemoveTempSpawns(client:GetPos(), radius)
|
||||
return "Удалено " .. count .. " временных спавнов."
|
||||
end
|
||||
})
|
||||
|
||||
-- Doomsday Commands
|
||||
ix.command.Add("DoomsdayStart", {
|
||||
description = "Запустить ивент Судная Ночь.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (DoomsdayNight.active) then
|
||||
return "Судная Ночь уже активна!"
|
||||
end
|
||||
DoomsdayNight:StartEvent(client)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("DoomsdayStop", {
|
||||
description = "Остановить ивент Судная Ночь.",
|
||||
privilege = "Manage Events",
|
||||
adminOnly = true,
|
||||
OnRun = function(self, client)
|
||||
if (!DoomsdayNight.active) then
|
||||
return "Судная Ночь не активна!"
|
||||
end
|
||||
DoomsdayNight:EndEvent(client, true)
|
||||
end
|
||||
})
|
||||
@@ -0,0 +1,184 @@
|
||||
DoomsdayNight = DoomsdayNight or {}
|
||||
DoomsdayNight.active = false
|
||||
DoomsdayNight.start_time = 0
|
||||
DoomsdayNight.participants = {}
|
||||
DoomsdayNight.spectators = {}
|
||||
DoomsdayNight.spawn_positions = {}
|
||||
|
||||
DoomsdayNight.weapons = {
|
||||
"tfa_ins2_ak74", "tfa_ins2_m4a1", "tfa_ins2_ak12", "tfa_ins2_m16a4", "tfa_ins2_scar_h_ssr",
|
||||
"tfa_ins2_m40a1", "tfa_ins2_mosin", "tfa_ins2_svd", "tfa_ins2_m590", "tfa_ins2_toz",
|
||||
"tfa_ins2_glock_17", "tfa_ins2_m9", "tfa_ins2_makarov", "tfa_ins2_mp5", "tfa_ins2_ump45", "tfa_ins2_mp40"
|
||||
}
|
||||
|
||||
util.AddNetworkString("doomsday_night_notification")
|
||||
util.AddNetworkString("doomsday_night_hud_update")
|
||||
|
||||
function DoomsdayNight:Init()
|
||||
SetGlobalBool("doomsday_night_active", false)
|
||||
SetGlobalInt("doomsday_night_start_time", 0)
|
||||
SetGlobalInt("doomsday_night_alive_count", 0)
|
||||
self:GenerateSpawnPositions()
|
||||
end
|
||||
|
||||
function DoomsdayNight:GenerateSpawnPositions()
|
||||
local map = game.GetMap()
|
||||
local map_positions = {
|
||||
["gm_construct"] = {
|
||||
Vector(980, -1500, -79), Vector(-700, 1000, 200), Vector(1500, 800, 100), Vector(-1200, -800, 50)
|
||||
},
|
||||
["rp_valkyrie_forest_v1"] = {
|
||||
Vector(5000, 3000, 200), Vector(-5000, -3000, 150), Vector(8000, -2000, 100), Vector(-8000, 2000, 180)
|
||||
}
|
||||
}
|
||||
self.spawn_positions = map_positions[map] or { Vector(1000, 1000, 100), Vector(-1000, -1000, 100) }
|
||||
end
|
||||
|
||||
function DoomsdayNight:StartEvent(ply)
|
||||
local players = player.GetAll()
|
||||
if (#players < 2) then
|
||||
ply:Notify("Недостаточно игроков для начала Судной Ночи!")
|
||||
return
|
||||
end
|
||||
|
||||
self.active = true
|
||||
self.start_time = CurTime()
|
||||
self.participants = {}
|
||||
self.spectators = {}
|
||||
|
||||
SetGlobalBool("doomsday_night_active", true)
|
||||
SetGlobalInt("doomsday_night_start_time", CurTime())
|
||||
|
||||
for _, player in ipairs(players) do
|
||||
if (IsValid(player)) then self:PreparePlayer(player) end
|
||||
end
|
||||
|
||||
self:BroadcastMessage("🌙 СУДНАЯ НОЧЬ НАЧАЛАСЬ! 🌙\nСражайтесь до последнего выжившего!\nВыдано случайное оружие.")
|
||||
|
||||
timer.Simple(3, function() if (self.active) then self:TeleportPlayers() end end)
|
||||
self:UpdateHUD()
|
||||
end
|
||||
|
||||
function DoomsdayNight:PreparePlayer(ply)
|
||||
local steamid = ply:SteamID()
|
||||
self.participants[steamid] = {
|
||||
start_pos = ply:GetPos(),
|
||||
join_time = CurTime(),
|
||||
kills = 0,
|
||||
nick = ply:Nick(),
|
||||
alive = true
|
||||
}
|
||||
ply:StripWeapons()
|
||||
self:GiveRandomWeapons(ply)
|
||||
end
|
||||
|
||||
function DoomsdayNight:GiveRandomWeapons(ply)
|
||||
if (!IsValid(ply) or !ply:Alive()) then return end
|
||||
ply:Give("ix_hands")
|
||||
|
||||
local weapons_to_give = {}
|
||||
local used = {}
|
||||
for i = 1, math.random(2, 3) do
|
||||
local w = self.weapons[math.random(1, #self.weapons)]
|
||||
if (!used[w]) then
|
||||
used[w] = true
|
||||
table.insert(weapons_to_give, w)
|
||||
end
|
||||
end
|
||||
|
||||
for _, w_class in ipairs(weapons_to_give) do
|
||||
local weapon = ply:Give(w_class)
|
||||
if (IsValid(weapon)) then
|
||||
ply:GiveAmmo(500, weapon:GetPrimaryAmmoType(), true)
|
||||
end
|
||||
end
|
||||
|
||||
ply:Give("weapon_frag")
|
||||
ply:GiveAmmo(2, "Grenade", true)
|
||||
ply:SetHealth(100)
|
||||
ply:SetArmor(100)
|
||||
end
|
||||
|
||||
function DoomsdayNight:TeleportPlayers()
|
||||
local players = {}
|
||||
for steamid, data in pairs(self.participants) do
|
||||
local ply = player.GetBySteamID(steamid)
|
||||
if (IsValid(ply)) then table.insert(players, ply) end
|
||||
end
|
||||
|
||||
local positions = table.Copy(self.spawn_positions)
|
||||
for i, ply in ipairs(players) do
|
||||
local spawn_pos = positions[((i - 1) % #positions) + 1]
|
||||
ply:SetPos(spawn_pos)
|
||||
ply:SetEyeAngles(Angle(0, math.random(0, 360), 0))
|
||||
end
|
||||
|
||||
SetGlobalInt("doomsday_night_alive_count", #players)
|
||||
self:BroadcastMessage("⚡ Игроки телепортированы! К БОЮ!")
|
||||
end
|
||||
|
||||
function DoomsdayNight:EndEvent(ply, cancelled)
|
||||
if (!self.active) then return end
|
||||
self.active = false
|
||||
SetGlobalBool("doomsday_night_active", false)
|
||||
|
||||
for steamid, _ in pairs(self.spectators) do
|
||||
local spec_ply = player.GetBySteamID(steamid)
|
||||
if (IsValid(spec_ply)) then
|
||||
spec_ply:UnSpectate()
|
||||
spec_ply:Spawn()
|
||||
end
|
||||
end
|
||||
|
||||
self:BroadcastMessage(cancelled and "🛑 СУДНАЯ НОЧЬ ОТМЕНЕНА" or "🏆 СУДНАЯ НОЧЬ ЗАВЕРШЕНА")
|
||||
self.participants = {}
|
||||
self.spectators = {}
|
||||
self:UpdateHUD()
|
||||
end
|
||||
|
||||
function DoomsdayNight:OnPlayerKilled(victim, attacker)
|
||||
if (!self.active) then return end
|
||||
local data = self.participants[victim:SteamID()]
|
||||
if (!data) then return end
|
||||
|
||||
data.alive = false
|
||||
if (IsValid(attacker) and attacker:IsPlayer()) then
|
||||
local attacker_data = self.participants[attacker:SteamID()]
|
||||
if (attacker_data) then attacker_data.kills = attacker_data.kills + 1 end
|
||||
end
|
||||
|
||||
timer.Simple(2, function()
|
||||
if (IsValid(victim)) then
|
||||
self.spectators[victim:SteamID()] = true
|
||||
victim:Spectate(OBS_MODE_ROAMING)
|
||||
victim:Notify("👁️ Вы перешли в режим наблюдения до конца ивента.")
|
||||
end
|
||||
end)
|
||||
|
||||
local alive_count = 0
|
||||
for _, d in pairs(self.participants) do
|
||||
if (d.alive) then alive_count = alive_count + 1 end
|
||||
end
|
||||
SetGlobalInt("doomsday_night_alive_count", alive_count)
|
||||
|
||||
if (alive_count <= 1) then
|
||||
timer.Simple(3, function() if (self.active) then self:EndEvent() end end)
|
||||
end
|
||||
end
|
||||
|
||||
function DoomsdayNight:BroadcastMessage(msg)
|
||||
for _, ply in ipairs(player.GetAll()) do ply:ChatPrint(msg) end
|
||||
end
|
||||
|
||||
function DoomsdayNight:UpdateHUD()
|
||||
net.Start("doomsday_night_hud_update")
|
||||
net.WriteBool(self.active)
|
||||
net.WriteInt(self.start_time, 32)
|
||||
net.Broadcast()
|
||||
end
|
||||
|
||||
-- Hook aliases for PLUGIN
|
||||
function PLUGIN:DoomsdayNight_Init() DoomsdayNight:Init() end
|
||||
function PLUGIN:DoomsdayNight_PlayerDeath(victim, inflictor, attacker) DoomsdayNight:OnPlayerKilled(victim, attacker) end
|
||||
|
||||
hook.Add("InitPostEntity", "DoomsdayNight_Init", function() DoomsdayNight:Init() end)
|
||||
@@ -0,0 +1,360 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.active = PLUGIN.active or false
|
||||
PLUGIN.start_time = PLUGIN.start_time or 0
|
||||
PLUGIN.participants = PLUGIN.participants or {}
|
||||
PLUGIN.eliminated_players = PLUGIN.eliminated_players or {}
|
||||
PLUGIN.initiator = PLUGIN.initiator or nil
|
||||
PLUGIN.tempSpawns = PLUGIN.tempSpawns or {} -- [factionUniqueID] = { [specDef or "default"] = { {pos, ang}, ... } }
|
||||
|
||||
-- Zone Data
|
||||
PLUGIN.zone = PLUGIN.zone or {
|
||||
pos1 = nil,
|
||||
pos2 = nil,
|
||||
min = nil,
|
||||
max = nil,
|
||||
shrinking = false,
|
||||
center = nil,
|
||||
shrink_speed = 1,
|
||||
min_size = 500
|
||||
}
|
||||
|
||||
-- Networking
|
||||
util.AddNetworkString("events_system_notification")
|
||||
util.AddNetworkString("events_system_hud_update")
|
||||
util.AddNetworkString("events_system_zone_update")
|
||||
|
||||
-- Initialization
|
||||
function PLUGIN:InitPostEntity()
|
||||
SetGlobalBool("events_system_active", false)
|
||||
SetGlobalInt("events_system_start_time", 0)
|
||||
|
||||
self.tempSpawns = self:GetData() or {}
|
||||
end
|
||||
|
||||
function PLUGIN:SaveTempSpawns()
|
||||
self:SetData(self.tempSpawns)
|
||||
end
|
||||
|
||||
-- Permissions Check (using ix.config)
|
||||
function PLUGIN:HasPermission(ply)
|
||||
if (!IsValid(ply)) then return false end
|
||||
if (ply:IsSuperAdmin()) then return true end
|
||||
|
||||
local allowed = string.Split(ix.config.Get("eventAllowedRanks", ""), ",")
|
||||
local userGroup = ply:GetUserGroup()
|
||||
|
||||
for _, rank in ipairs(allowed) do
|
||||
if (string.lower(userGroup) == string.lower(string.Trim(rank))) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Zone Logic
|
||||
function PLUGIN:SetZonePoint1(ply)
|
||||
self.zone.pos1 = ply:GetPos()
|
||||
self:SendMessage(ply, "✅ Точка зоны 1 установлена: " .. tostring(self.zone.pos1))
|
||||
if (self.zone.pos2) then self:CalculateZoneBounds() end
|
||||
end
|
||||
|
||||
function PLUGIN:SetZonePoint2(ply)
|
||||
self.zone.pos2 = ply:GetPos()
|
||||
self:SendMessage(ply, "✅ Точка зоны 2 установлена: " .. tostring(self.zone.pos2))
|
||||
if (self.zone.pos1) then self:CalculateZoneBounds() end
|
||||
end
|
||||
|
||||
function PLUGIN:CalculateZoneBounds()
|
||||
if (!self.zone.pos1 or !self.zone.pos2) then return end
|
||||
|
||||
self.zone.min = Vector(
|
||||
math.min(self.zone.pos1.x, self.zone.pos2.x),
|
||||
math.min(self.zone.pos1.y, self.zone.pos2.y),
|
||||
math.min(self.zone.pos1.z, self.zone.pos2.z)
|
||||
)
|
||||
|
||||
self.zone.max = Vector(
|
||||
math.max(self.zone.pos1.x, self.zone.pos2.x),
|
||||
math.max(self.zone.pos1.y, self.zone.pos2.y),
|
||||
math.max(self.zone.pos1.z, self.zone.pos2.z)
|
||||
)
|
||||
|
||||
self:BroadcastZone()
|
||||
local size = self.zone.max - self.zone.min
|
||||
self:BroadcastMessage(string.format("🎯 Зона ивента установлена! Размер: %.0f x %.0f x %.0f", size.x, size.y, size.z))
|
||||
end
|
||||
|
||||
function PLUGIN:BroadcastZone()
|
||||
if (!self.zone.min or !self.zone.max) then return end
|
||||
net.Start("events_system_zone_update")
|
||||
net.WriteBool(true)
|
||||
net.WriteVector(self.zone.min)
|
||||
net.WriteVector(self.zone.max)
|
||||
net.Broadcast()
|
||||
end
|
||||
|
||||
function PLUGIN:ClearZone(ply)
|
||||
self.zone.pos1 = nil
|
||||
self.zone.pos2 = nil
|
||||
self.zone.min = nil
|
||||
self.zone.max = nil
|
||||
self.zone.shrinking = false
|
||||
|
||||
if (timer.Exists("EventsSystem_ZoneShrink")) then
|
||||
timer.Remove("EventsSystem_ZoneShrink")
|
||||
end
|
||||
|
||||
net.Start("events_system_zone_update")
|
||||
net.WriteBool(false)
|
||||
net.Broadcast()
|
||||
|
||||
if (IsValid(ply)) then
|
||||
self:SendMessage(ply, "🗑️ Зона ивента очищена.")
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:StartZoneShrinking(ply)
|
||||
if (!self.zone.min or !self.zone.max) then
|
||||
if (IsValid(ply)) then self:SendMessage(ply, "❌ Сначала установите зону!") end
|
||||
return
|
||||
end
|
||||
|
||||
if (self.zone.shrinking) then
|
||||
if (IsValid(ply)) then self:SendMessage(ply, "⚠️ Зона уже сужается!") end
|
||||
return
|
||||
end
|
||||
|
||||
self.zone.center = (self.zone.min + self.zone.max) / 2
|
||||
self.zone.shrinking = true
|
||||
|
||||
timer.Create("EventsSystem_ZoneShrink", 0.1, 0, function()
|
||||
self:ShrinkZone()
|
||||
end)
|
||||
|
||||
self:BroadcastMessage("🔴 ВНИМАНИЕ! Зона начала сужаться к центру!")
|
||||
for _, player in ipairs(player.GetAll()) do
|
||||
player:EmitSound("ambient/alarms/warningbell1.wav", 75, 100)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:StopZoneShrinking(ply)
|
||||
if (!self.zone.shrinking) then return end
|
||||
self.zone.shrinking = false
|
||||
timer.Remove("EventsSystem_ZoneShrink")
|
||||
|
||||
local size = self.zone.max - self.zone.min
|
||||
self:BroadcastMessage(string.format("⏸️ Сужение зоны остановлено! Текущий размер: %.0f x %.0f", size.x, size.y))
|
||||
end
|
||||
|
||||
function PLUGIN:ShrinkZone()
|
||||
if (!self.zone.shrinking or !self.zone.min or !self.zone.max or !self.zone.center) then return end
|
||||
|
||||
local shrink_amount = self.zone.shrink_speed * 0.1
|
||||
local new_min = Vector(self.zone.min.x + shrink_amount, self.zone.min.y + shrink_amount, self.zone.min.z)
|
||||
local new_max = Vector(self.zone.max.x - shrink_amount, self.zone.max.y - shrink_amount, self.zone.max.z)
|
||||
|
||||
if (new_max.x - new_min.x <= self.zone.min_size or new_max.y - new_min.y <= self.zone.min_size) then
|
||||
self:StopZoneShrinking(nil)
|
||||
self:BroadcastMessage("❗ Зона достигла минимального размера!")
|
||||
return
|
||||
end
|
||||
|
||||
self.zone.min = new_min
|
||||
self.zone.max = new_max
|
||||
self:BroadcastZone()
|
||||
end
|
||||
|
||||
function PLUGIN:IsInZone(pos)
|
||||
if (!self.zone.min or !self.zone.max) then return true end
|
||||
return pos.x >= self.zone.min.x and pos.x <= self.zone.max.x and
|
||||
pos.y >= self.zone.min.y and pos.y <= self.zone.max.y and
|
||||
pos.z >= self.zone.min.z and pos.z <= self.zone.max.z
|
||||
end
|
||||
|
||||
function PLUGIN:CheckZoneBoundaries()
|
||||
if (!self.active) then return end
|
||||
for steamid, data in pairs(self.participants) do
|
||||
local ply = player.GetBySteamID(steamid)
|
||||
if (IsValid(ply) and ply:Alive()) then
|
||||
if (!self:IsInZone(ply:GetPos())) then
|
||||
ply:TakeDamage(10, ply, ply)
|
||||
ply:Notify("⚠️ ВНИМАНИЕ! Вы вне зоны ивента! Немедленно вернитесь!")
|
||||
ply:EmitSound("buttons/button10.wav", 75, 100)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Event Control
|
||||
function PLUGIN:StartEvent(ply)
|
||||
self.active = true
|
||||
self.start_time = CurTime()
|
||||
self.participants = {}
|
||||
self.eliminated_players = {}
|
||||
self.initiator = ply
|
||||
|
||||
SetGlobalBool("events_system_active", true)
|
||||
SetGlobalInt("events_system_start_time", CurTime())
|
||||
|
||||
local initiator_name = IsValid(ply) and ply:Nick() or "Администратор"
|
||||
local start_message = "⚔️ БОЕВОЙ ИВЕНТ НАЧАЛСЯ! ⚔️\nОрганизатор: " .. initiator_name .. "\nКак участвовать: Используйте /EventJoin\nЦель: Выжить и победить!"
|
||||
self:BroadcastMessage(start_message)
|
||||
|
||||
for _, player in ipairs(player.GetAll()) do
|
||||
player:EmitSound(self.Config.sounds.event_start, 75, 100, 0.8)
|
||||
end
|
||||
|
||||
self:UpdateHUD()
|
||||
timer.Create("EventsSystem_ZoneCheck", 1, 0, function() self:CheckZoneBoundaries() end)
|
||||
end
|
||||
|
||||
function PLUGIN:EndEvent(ply, cancelled)
|
||||
if (!self.active) then return end
|
||||
|
||||
local stats_lines = {
|
||||
"╔═══════════════════════════════════════╗",
|
||||
"║ 📊 ФИНАЛЬНАЯ СТАТИСТИКА 📊 ║",
|
||||
"╚═══════════════════════════════════════╝",
|
||||
string.format("👥 Всего участников: %d", table.Count(self.participants) + table.Count(self.eliminated_players))
|
||||
}
|
||||
|
||||
self.active = false
|
||||
SetGlobalBool("events_system_active", false)
|
||||
SetGlobalInt("events_system_start_time", 0)
|
||||
|
||||
local end_message = cancelled and "🛑 ИВЕНТ ОТМЕНЕН АДМИНИСТРАТОРОМ" or "🏆 ИВЕНТ ЗАВЕРШЕН 🏆"
|
||||
self:BroadcastMessage(end_message)
|
||||
for _, line in ipairs(stats_lines) do self:BroadcastMessage(line) end
|
||||
|
||||
for _, player in ipairs(player.GetAll()) do
|
||||
player:EmitSound(self.Config.sounds.event_end, 75, 100, 0.6)
|
||||
end
|
||||
|
||||
self.participants = {}
|
||||
self.eliminated_players = {}
|
||||
timer.Remove("EventsSystem_ZoneCheck")
|
||||
self:UpdateHUD()
|
||||
end
|
||||
|
||||
function PLUGIN:AddParticipant(ply)
|
||||
if (!self.active or !IsValid(ply)) then return end
|
||||
self.participants[ply:SteamID()] = {
|
||||
join_time = CurTime(),
|
||||
kills = 0,
|
||||
nick = ply:Nick()
|
||||
}
|
||||
ply:Notify("✅ ВЫ ПРИСОЕДИНИЛИСЬ К ИВЕНТУ!")
|
||||
ply:EmitSound(self.Config.sounds.player_join, 50, 100, 0.5)
|
||||
end
|
||||
|
||||
function PLUGIN:AddAllPlayers(admin_ply)
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
self:AddParticipant(ply)
|
||||
end
|
||||
self:BroadcastMessage("📢 Все игроки добавлены в ивент!")
|
||||
end
|
||||
|
||||
-- Hooks
|
||||
function PLUGIN:PlayerDeath(victim, inflictor, attacker)
|
||||
if (!self.active) then return end
|
||||
local steamid = victim:SteamID()
|
||||
local data = self.participants[steamid]
|
||||
if (!data) then return end
|
||||
|
||||
if (IsValid(attacker) and attacker:IsPlayer()) then
|
||||
local attacker_data = self.participants[attacker:SteamID()]
|
||||
if (attacker_data) then attacker_data.kills = attacker_data.kills + 1 end
|
||||
end
|
||||
|
||||
self.eliminated_players[steamid] = data
|
||||
self.participants[steamid] = nil
|
||||
|
||||
victim:Notify("💀 Вы выбыли из ивента!")
|
||||
self:BroadcastMessage(string.format("💀 %s выбыл! Убийств: %d", victim:Nick(), data.kills))
|
||||
|
||||
timer.Simple(0.5, function() self:CheckEventEnd() end)
|
||||
end
|
||||
|
||||
function PLUGIN:CheckEventEnd()
|
||||
if (!self.active) then return end
|
||||
if (table.Count(self.participants) <= 1) then
|
||||
self:EndEvent()
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:PlayerDisconnected(ply)
|
||||
if (!self.active) then return end
|
||||
self.participants[ply:SteamID()] = nil
|
||||
end
|
||||
|
||||
-- Messaging
|
||||
function PLUGIN:SendMessage(ply, message)
|
||||
if (!IsValid(ply)) then return end
|
||||
net.Start("events_system_notification")
|
||||
net.WriteString(message)
|
||||
net.Send(ply)
|
||||
end
|
||||
|
||||
function PLUGIN:BroadcastMessage(message)
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
ply:ChatPrint(message)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:UpdateHUD()
|
||||
net.Start("events_system_hud_update")
|
||||
net.WriteBool(self.active)
|
||||
net.WriteInt(self.start_time, 32)
|
||||
net.Broadcast()
|
||||
end
|
||||
|
||||
-- Temporary Spawns Implementation
|
||||
function PLUGIN:AddTempSpawn(factionID, specDef, pos, ang)
|
||||
specDef = specDef or "default"
|
||||
self.tempSpawns[factionID] = self.tempSpawns[factionID] or {}
|
||||
self.tempSpawns[factionID][specDef] = self.tempSpawns[factionID][specDef] or {}
|
||||
|
||||
table.insert(self.tempSpawns[factionID][specDef], {pos = pos, ang = ang})
|
||||
self:SaveTempSpawns()
|
||||
end
|
||||
|
||||
function PLUGIN:RemoveTempSpawns(pos, radius)
|
||||
local count = 0
|
||||
for factionID, specs in pairs(self.tempSpawns) do
|
||||
for specDef, points in pairs(specs) do
|
||||
for k, v in pairs(points) do
|
||||
if (v.pos:Distance(pos) <= radius) then
|
||||
points[k] = nil
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if (count > 0) then self:SaveTempSpawns() end
|
||||
return count
|
||||
end
|
||||
|
||||
function PLUGIN:PostPlayerLoadout(client)
|
||||
if (!self.active) then return end
|
||||
|
||||
local char = client:GetCharacter()
|
||||
if (!char) then return end
|
||||
|
||||
local faction = ix.faction.indices[client:Team()]
|
||||
if (!faction) then return end
|
||||
|
||||
local factionID = faction.uniqueID
|
||||
local podr = char:GetPodr()
|
||||
local factionTable = ix.faction.Get(client:Team())
|
||||
local specDef = (factionTable and factionTable.Podr and factionTable.Podr[podr]) and factionTable.Podr[podr].spec_def or "default"
|
||||
|
||||
local points = (self.tempSpawns[factionID] and self.tempSpawns[factionID][specDef]) or (self.tempSpawns[factionID] and self.tempSpawns[factionID]["default"])
|
||||
|
||||
if (points and !table.IsEmpty(points)) then
|
||||
local point = table.Random(points)
|
||||
client:SetPos(point.pos)
|
||||
client:SetEyeAngles(point.ang)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,444 @@
|
||||
-- Extended Spawnmenu - Legacy Addons (старые аддоны и загрузки)
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
language.Add("spawnmenu.category.addonslegacy", "Addons - Legacy")
|
||||
language.Add("spawnmenu.category.downloads", "Downloads")
|
||||
|
||||
local function AddRecursive( pnl, folder )
|
||||
local files, folders = file.Find( folder .. "*", "MOD" )
|
||||
|
||||
for k, v in pairs( files or {} ) do
|
||||
if ( !string.EndsWith( v, ".mdl" ) ) then continue end
|
||||
|
||||
local cp = spawnmenu.GetContentType( "model" )
|
||||
if ( cp ) then
|
||||
local mdl = folder .. v
|
||||
mdl = string.sub( mdl, string.find( mdl, "models/" ), string.len( mdl ) )
|
||||
mdl = string.gsub( mdl, "models/models/", "models/" )
|
||||
cp( pnl, { model = mdl } )
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in pairs( folders or {} ) do AddRecursive( pnl, folder .. v .. "/" ) end
|
||||
end
|
||||
|
||||
local function CountRecursive( folder )
|
||||
local files, folders = file.Find( folder .. "*", "MOD" )
|
||||
local val = 0
|
||||
|
||||
for k, v in pairs( files or {} ) do if ( string.EndsWith( v, ".mdl" ) ) then val = val + 1 end end
|
||||
for k, v in pairs( folders or {} ) do val = val + CountRecursive( folder .. v .. "/" ) end
|
||||
return val
|
||||
end
|
||||
|
||||
hook.Add( "PopulateContent", "LegacyAddonProps", function( pnlContent, tree, node )
|
||||
|
||||
if ( !IsValid( node ) or !IsValid( pnlContent ) ) then
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR LEGACY ADDONS!!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR LEGACY ADDONS!!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR LEGACY ADDONS!!!" )
|
||||
return
|
||||
end
|
||||
|
||||
local ViewPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
ViewPanel:SetVisible( false )
|
||||
|
||||
local addons = {}
|
||||
|
||||
local _files, folders = file.Find( "addons/*", "MOD" )
|
||||
for _, f in pairs( folders ) do
|
||||
|
||||
if ( !file.IsDir( "addons/" .. f .. "/models/", "MOD" ) ) then continue end
|
||||
|
||||
local count = CountRecursive( "addons/" .. f .. "/models/", "MOD" )
|
||||
if ( count == 0 ) then continue end
|
||||
|
||||
table.insert( addons, {
|
||||
name = f,
|
||||
count = count,
|
||||
path = "addons/" .. f .. "/models/"
|
||||
} )
|
||||
|
||||
end
|
||||
|
||||
local LegacyAddons = node:AddNode( "#spawnmenu.category.addonslegacy", "icon16/folder_database.png" )
|
||||
for _, f in SortedPairsByMemberValue( addons, "name" ) do
|
||||
|
||||
local models = LegacyAddons:AddNode( f.name .. " (" .. f.count .. ")", "icon16/bricks.png" )
|
||||
models.DoClick = function()
|
||||
ViewPanel:Clear( true )
|
||||
AddRecursive( ViewPanel, f.path )
|
||||
pnlContent:SwitchPanel( ViewPanel )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--[[ -------------------------- DOWNLOADS -------------------------- ]]
|
||||
|
||||
local fi, fo = file.Find( "download/models", "MOD" )
|
||||
if ( !fi && !fo ) then return end
|
||||
|
||||
local Downloads = node:AddFolder( "#spawnmenu.category.downloads", "download/models", "MOD", false, false, "*.*" )
|
||||
Downloads:SetIcon( "icon16/folder_database.png" )
|
||||
|
||||
Downloads.OnNodeSelected = function( self, selectedNode )
|
||||
ViewPanel:Clear( true )
|
||||
|
||||
local path = selectedNode:GetFolder()
|
||||
|
||||
if ( !string.EndsWith( path, "/" ) && string.len( path ) > 1 ) then path = path .. "/" end
|
||||
local path_mdl = string.sub( path, string.find( path, "/models/" ) + 1 )
|
||||
|
||||
for k, v in pairs( file.Find( path .. "/*.mdl", selectedNode:GetPathID() ) ) do
|
||||
|
||||
local cp = spawnmenu.GetContentType( "model" )
|
||||
if ( cp ) then
|
||||
cp( ViewPanel, { model = path_mdl .. "/" .. v } )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
pnlContent:SwitchPanel( ViewPanel )
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
--[[ -------------------------------------------------------------------------- The addon info -------------------------------------------------------------------------- ]]
|
||||
|
||||
concommand.Add( "extsm_addoninfo", function()
|
||||
local frame = vgui.Create( "DFrame" )
|
||||
frame:SetSize( ScrW() - 100, ScrH() - 100 )
|
||||
frame:Center()
|
||||
frame:MakePopup()
|
||||
|
||||
local sp = frame:Add( "DScrollPanel" )
|
||||
sp:Dock( FILL )
|
||||
|
||||
sp:Add( "rb655_addonInfo" )
|
||||
end )
|
||||
|
||||
hook.Add( "AddToolMenuCategories", "LegacyAddonPropsInfoCategory", function()
|
||||
spawnmenu.AddToolCategory( "Utilities", "Robotboy655", "#Robotboy655" )
|
||||
end )
|
||||
|
||||
hook.Add( "PopulateToolMenu", "LegacyAddonPropsInfoThing", function()
|
||||
spawnmenu.AddToolMenuOption( "Utilities", "Robotboy655", "LegacyInfoPanel", "Addon Information", "", "", function( panel )
|
||||
panel:ClearControls()
|
||||
panel:Button( "Open addon data window", "extsm_addoninfo" )
|
||||
end )
|
||||
end )
|
||||
|
||||
----------------------------------
|
||||
|
||||
function ScreenScaleH( size )
|
||||
return size * ( ScrH() / 480.0 )
|
||||
end
|
||||
|
||||
surface.CreateFont( "AddonInfo_Header", {
|
||||
font = "Helvetica",
|
||||
size = ScreenScaleH( 24 ),
|
||||
weight = 1000
|
||||
} )
|
||||
|
||||
surface.CreateFont( "AddonInfo_Text", {
|
||||
font = "Helvetica",
|
||||
size = ScreenScaleH( 9 ),
|
||||
weight = 1000
|
||||
} )
|
||||
|
||||
surface.CreateFont( "AddonInfo_Small", {
|
||||
font = "Helvetica",
|
||||
size = ScreenScaleH( 8 )
|
||||
} )
|
||||
|
||||
local function GetWorkshopLeftovers()
|
||||
|
||||
local subscriptions = {}
|
||||
|
||||
for id, t in pairs( engine.GetAddons() ) do
|
||||
subscriptions[ tonumber( t.wsid ) ] = true
|
||||
end
|
||||
|
||||
local t = {}
|
||||
for id, fileh in pairs( file.Find( "addons/*.gma", "MOD" ) ) do
|
||||
local a = string.StripExtension( fileh )
|
||||
a = string.Explode( "_", a )
|
||||
a = tonumber( a[ #a ] )
|
||||
if ( !subscriptions[ a ] ) then
|
||||
table.insert( t, fileh )
|
||||
end
|
||||
end
|
||||
|
||||
return t
|
||||
|
||||
end
|
||||
|
||||
local function GetSize( b )
|
||||
b = b / 1000
|
||||
|
||||
if ( b < 1000 ) then
|
||||
return math.floor( b * 10 ) / 10 .. " KB"
|
||||
end
|
||||
|
||||
b = b / 1000
|
||||
|
||||
if ( b < 1000 ) then
|
||||
return math.floor( b * 10 ) / 10 .. " MB"
|
||||
end
|
||||
|
||||
b = b / 1000
|
||||
|
||||
return math.floor( b * 10 ) / 10 .. " GB"
|
||||
end
|
||||
|
||||
local function DrawText( txt, font, x, y, clr )
|
||||
draw.SimpleText( txt, font, x, y, clr )
|
||||
|
||||
surface.SetFont( font )
|
||||
return surface.GetTextSize( txt )
|
||||
end
|
||||
|
||||
local PANEL = {}
|
||||
|
||||
function PANEL:Init()
|
||||
self.Computed = false
|
||||
end
|
||||
|
||||
function PANEL:Compute()
|
||||
|
||||
self.WorkshopSize = 0
|
||||
for id, fle in pairs( file.Find( "addons/*.gma", "MOD" ) ) do
|
||||
self.WorkshopSize = self.WorkshopSize + ( file.Size( "addons/" .. fle, "MOD" ) or 0 )
|
||||
end
|
||||
|
||||
self.WorkshopWaste = 0
|
||||
self.WorkshopWasteFiles = {}
|
||||
for id, fle in pairs( GetWorkshopLeftovers() ) do
|
||||
self.WorkshopWaste = self.WorkshopWaste + ( file.Size( "addons/" .. fle, "MOD" ) or 0 )
|
||||
table.insert( self.WorkshopWasteFiles, { "addons/" .. fle, ( file.Size( "addons/" .. fle, "MOD" ) or 0 ) } )
|
||||
end
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
local _files, folders = file.Find( "addons/*", "MOD" )
|
||||
|
||||
self.LegacyAddons = {}
|
||||
for k, v in pairs( folders or {} ) do
|
||||
self.LegacyAddons[ "addons/" .. v .. "/" ] = "Installed"
|
||||
|
||||
if ( file.IsDir( "addons/" .. v .. "/models/", "MOD" ) ) then
|
||||
self.LegacyAddons[ "addons/" .. v .. "/" ] = "Installed (Has Models)"
|
||||
end
|
||||
|
||||
local _fi, fo = file.Find( "addons/" .. v .. "/*", "MOD" )
|
||||
if ( table.Count( fo or {} ) < 1 ) then
|
||||
self.LegacyAddons[ "addons/" .. v .. "/" ] = "Installed (Empty)"
|
||||
end
|
||||
|
||||
if ( !file.IsDir( "addons/" .. v .. "/models/", "MOD" ) && !file.IsDir( "addons/" .. v .. "/materials/", "MOD" ) && !file.IsDir( "addons/" .. v .. "/lua/", "MOD" ) && !file.IsDir( "addons/" .. v .. "/sound/", "MOD" ) ) then
|
||||
self.LegacyAddons[ "addons/" .. v .. "/" ] = "Installed Incorrectly!"
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
local luaFiles = file.Find( "cache/lua/*", "MOD" ) -- Too many files to count actual size!
|
||||
self.LuaCacheSize = #luaFiles * 1400
|
||||
self.LuaCacheFiles = #luaFiles
|
||||
|
||||
local wsFiles = file.Find( "cache/workshop/*", "MOD" )
|
||||
self.WSCacheSize = 0
|
||||
for id, fle in pairs( wsFiles ) do
|
||||
self.WSCacheSize = self.WSCacheSize + ( file.Size( "cache/workshop/" .. fle, "MOD" ) or 0 )
|
||||
end
|
||||
self.WSCacheFiles = #wsFiles
|
||||
|
||||
self.Computed = true
|
||||
|
||||
end
|
||||
|
||||
function PANEL:Paint( w, h )
|
||||
|
||||
if ( !self.Computed ) then
|
||||
self:Compute()
|
||||
end
|
||||
|
||||
local txtW = self:GetParent():GetWide()
|
||||
local txtH = 0
|
||||
|
||||
-- -----------------------
|
||||
|
||||
local tW, tH = DrawText( "Cache Sizes", "AddonInfo_Header", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
local localH = 0
|
||||
local localW = 0
|
||||
|
||||
-- -----------------------
|
||||
|
||||
tW, tH = DrawText( "~" .. GetSize( self.LuaCacheSize or 0 ) .. " (" .. self.LuaCacheFiles .. " files)", "AddonInfo_Small", 0, txtH + localH, Color( 220, 220, 220 ) )
|
||||
localH = localH + tH
|
||||
localW = math.max( localW, tW )
|
||||
|
||||
tW, tH = DrawText( "~" .. GetSize( self.WSCacheSize or 0 ) .. " (" .. self.WSCacheFiles .. " files)", "AddonInfo_Small", 0, txtH + localH, Color( 220, 220, 220 ) )
|
||||
localH = localH + tH
|
||||
localW = math.max( localW, tW )
|
||||
|
||||
-- -----------------------
|
||||
|
||||
localW = localW + 25
|
||||
|
||||
tW, tH = DrawText( "Server Lua cache", "AddonInfo_Small", localW, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( "Workshop download cache", "AddonInfo_Small", localW, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
txtH = txtH + ScreenScaleH( 8 )
|
||||
tW, tH = DrawText( "Workshop Subscriptions", "AddonInfo_Header", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
tW, tH = DrawText( "Used Size: ", "AddonInfo_Text", 0, txtH, color_white )
|
||||
local maxW = tW
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( "Wasted Space: ", "AddonInfo_Text", 0, txtH, color_white )
|
||||
maxW = math.max( maxW, tW )
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( "Total Size: ", "AddonInfo_Text", 0, txtH, color_white )
|
||||
maxW = math.max( maxW, tW )
|
||||
txtH = txtH - tH * 2
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
tW, tH = DrawText( GetSize( ( self.WorkshopSize - self.WorkshopWaste ) or 0 ), "AddonInfo_Text", maxW, txtH, Color( 220, 220, 220 ) )
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( GetSize( self.WorkshopWaste or 0 ), "AddonInfo_Text", maxW, txtH, Color( 220, 220, 220 ) )
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( GetSize( self.WorkshopSize or 0 ), "AddonInfo_Text", maxW, txtH, Color( 220, 220, 220 ) )
|
||||
txtH = txtH + tH * 2
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
tW, tH = DrawText( "Files that aren't used: ( Safe to delete )", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
localH = 0
|
||||
localW = 0
|
||||
for id, t in pairs( self.WorkshopWasteFiles or {} ) do
|
||||
tW, tH = DrawText( GetSize( t[ 2 ] ) .. " ", "AddonInfo_Small", 0, txtH + localH, Color( 220, 220, 220 ) )
|
||||
localH = localH + tH
|
||||
localW = math.max( localW, tW )
|
||||
end
|
||||
|
||||
for id, t in pairs( self.WorkshopWasteFiles or {} ) do
|
||||
tW, tH = DrawText( t[ 1 ], "AddonInfo_Small", localW, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
end
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
tW, tH = DrawText( "Legacy Addons", "AddonInfo_Header", 0, txtH + ScreenScaleH( 8 ), color_white )
|
||||
txtH = txtH + tH + ScreenScaleH( 8 )
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
tW, tH = DrawText( "Legacy Addons with models:", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
|
||||
if ( table.Count( self.LegacyAddons or {} ) > 0 ) then
|
||||
local maxNameW = 0
|
||||
local oldH = txtH
|
||||
for path, status in pairs( self.LegacyAddons or {} ) do
|
||||
tW, tH = DrawText( path, "AddonInfo_Small", 0, txtH, color_white )
|
||||
maxNameW = math.max( maxNameW, tW )
|
||||
txtH = txtH + tH
|
||||
end
|
||||
|
||||
maxNameW = maxNameW + 25
|
||||
txtH = oldH
|
||||
|
||||
for path, status in pairs( self.LegacyAddons or {} ) do
|
||||
tW, tH = DrawText( status, "AddonInfo_Small", maxNameW, txtH, Color( 220, 220, 220 ) )
|
||||
txtH = txtH + tH
|
||||
end
|
||||
else
|
||||
tW, tH = DrawText( "None.", "AddonInfo_Small", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
end
|
||||
|
||||
if ( !system.IsWindows() ) then
|
||||
txtH = txtH + tH
|
||||
|
||||
tW, tH = DrawText( "OSX AND LINUX USERS BEWARE:", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
tW, tH = DrawText( "MAKE SURE ALL FILE AND FOLDER NAMES", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
tW, tH = DrawText( "IN ALL ADDONS ARE LOWERCASE ONLY", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
tW, tH = DrawText( "INCLUDING ALL SUB FOLDERS", "AddonInfo_Text", 0, txtH, color_white )
|
||||
txtH = txtH + tH
|
||||
end
|
||||
|
||||
txtH = txtH + tH
|
||||
|
||||
-- -------------------------------------------
|
||||
|
||||
self:SetSize( txtW, txtH )
|
||||
end
|
||||
|
||||
vgui.Register( "rb655_addonInfo", PANEL, "Panel" )
|
||||
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
|
||||
-- I spent too much time on this than I care to admit
|
||||
hook.Add( "PopulatePropMenu", "rb655_LoadLegacySpawnlists", function()
|
||||
|
||||
local sid = 0 --table.Count( spawnmenu.GetPropTable() )
|
||||
|
||||
--local added = false
|
||||
|
||||
for id, spawnlist in pairs( file.Find( "settings/spawnlist/*.txt", "MOD" ) ) do
|
||||
local content = file.Read( "settings/spawnlist/" .. spawnlist, "MOD" )
|
||||
if ( !content ) then continue end
|
||||
|
||||
--[[local is = string.find( content, "TableToKeyValues" )
|
||||
if ( is != nil ) then continue end
|
||||
|
||||
for id, t in pairs( spawnmenu.GetPropTable() ) do -- This somehow freezes the game when opening Q menu => FUCK THIS SHIT
|
||||
if ( t.name == "Legacy Spawnlists" ) then
|
||||
added = true
|
||||
sid = t.id
|
||||
end
|
||||
end
|
||||
|
||||
if ( !added ) then
|
||||
spawnmenu.AddPropCategory( "rb655_legacy_spawnlists", "Legacy Spawnlists", {}, "icon16/folder.png", sid, 0 )
|
||||
added = true
|
||||
end]]
|
||||
|
||||
content = util.KeyValuesToTable( content )
|
||||
|
||||
if ( !content.entries or content.contents ) then continue end
|
||||
|
||||
local contents = {}
|
||||
|
||||
for eid, entry in pairs( content.entries ) do
|
||||
if ( type( entry ) == "table" ) then entry = entry.model end
|
||||
table.insert( contents, { type = "model", model = entry } )
|
||||
end
|
||||
|
||||
if ( !content.information ) then content.information = { name = spawnlist } end
|
||||
|
||||
spawnmenu.AddPropCategory( "settings/spawnlist/" .. spawnlist, content.information.name, contents, "icon16/page.png", sid + id, sid )
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
@@ -0,0 +1,790 @@
|
||||
-- Extended Spawnmenu - Клиентская часть (звуки и материалы)
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
local cl_addTabs = CreateClientConVar("rb655_create_sm_tabs", "0", true, true)
|
||||
|
||||
--[[local function removeOldTabls()
|
||||
for k, v in pairs( g_SpawnMenu.CreateMenu.Items ) do
|
||||
if (v.Tab:GetText() == language.GetPhrase( "spawnmenu.category.npcs" ) or
|
||||
v.Tab:GetText() == language.GetPhrase( "spawnmenu.category.entities" ) or
|
||||
v.Tab:GetText() == language.GetPhrase( "spawnmenu.category.weapons" ) or
|
||||
v.Tab:GetText() == language.GetPhrase( "spawnmenu.category.vehicles" ) or
|
||||
v.Tab:GetText() == language.GetPhrase( "spawnmenu.category.postprocess" ) ) then
|
||||
g_SpawnMenu.CreateMenu:CloseTab( v.Tab, true )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu", function( pnlContent, tree, node )
|
||||
removeOldTabls() removeOldTabls() removeOldTabls() -- For some reason it doesn't work with only one call
|
||||
end )]]
|
||||
|
||||
local function getGameList()
|
||||
local games = engine.GetGames()
|
||||
table.insert( games, {
|
||||
title = "All",
|
||||
folder = "GAME",
|
||||
icon = "all",
|
||||
mounted = true
|
||||
} )
|
||||
table.insert( games, {
|
||||
title = "Garry's Mod",
|
||||
folder = "garrysmod",
|
||||
mounted = true
|
||||
} )
|
||||
return games
|
||||
end
|
||||
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
|
||||
local theSound = nil
|
||||
|
||||
function rb655_playsound( snd )
|
||||
|
||||
if ( theSound ) then theSound:Stop() end
|
||||
|
||||
theSound = CreateSound( LocalPlayer(), snd )
|
||||
theSound:Play()
|
||||
|
||||
end
|
||||
|
||||
net.Receive( "rb655_playsound", function( len )
|
||||
rb655_playsound( net.ReadString() )
|
||||
end )
|
||||
|
||||
spawnmenu.AddContentType( "sound", function( container, obj )
|
||||
if ( !obj.nicename ) then return end
|
||||
if ( !obj.spawnname ) then return end
|
||||
|
||||
local icon = vgui.Create( "ContentIcon", container )
|
||||
icon:SetContentType( "sound" )
|
||||
icon:SetSpawnName( obj.spawnname )
|
||||
icon:SetName( obj.nicename )
|
||||
icon:SetMaterial( "icon16/sound.png" )
|
||||
|
||||
icon.DoClick = function()
|
||||
rb655_playsound( obj.spawnname )
|
||||
end
|
||||
|
||||
icon.OpenMenu = function( icn )
|
||||
local menu = DermaMenu()
|
||||
menu:AddOption( "#spawnmenu.menu.copy", function() SetClipboardText( obj.spawnname ) end ):SetIcon( "icon16/page_copy.png" )
|
||||
menu:AddOption( "Play on all clients", function() RunConsoleCommand( "rb655_playsound_all", obj.spawnname ) end ):SetIcon( "icon16/sound.png" )
|
||||
menu:AddOption( "Stop all sounds", function() RunConsoleCommand( "stopsound" ) end ):SetIcon( "icon16/sound_mute.png" )
|
||||
menu:AddSpacer()
|
||||
menu:AddOption( "#spawnmenu.menu.delete", function() icn:Remove() hook.Run( "SpawnlistContentChanged", icn ) end ):SetIcon( "icon16/bin_closed.png" )
|
||||
menu:Open()
|
||||
end
|
||||
|
||||
if ( IsValid( container ) ) then
|
||||
container:Add( icon )
|
||||
end
|
||||
|
||||
return icon
|
||||
|
||||
end )
|
||||
|
||||
local function OnSndNodeSelected( self, node, name, path, pathid, icon, ViewPanel, pnlContent )
|
||||
|
||||
ViewPanel:Clear( true )
|
||||
|
||||
local Path = node:GetFolder()
|
||||
|
||||
local files = file.Find( Path .. "/*.wav", node:GetPathID() )
|
||||
files = table.Add( files, file.Find( Path .. "/*.mp3", node:GetPathID() ) )
|
||||
files = table.Add( files, file.Find( Path .. "/*.ogg", node:GetPathID() ) )
|
||||
|
||||
local offset = 0
|
||||
local limit = 512
|
||||
if ( node.offset ) then offset = node.offset or 0 end
|
||||
|
||||
for k, v in pairs( files ) do
|
||||
if ( k > limit + offset ) then
|
||||
if ( !node.Done ) then
|
||||
offset = offset + limit
|
||||
local mats = ( self.Parent or node ):AddNode( ( self.Text or node:GetText() ) .. " (" .. offset .. " - " .. offset + limit .. ")" )
|
||||
mats:SetFolder( node:GetFolder() )
|
||||
mats.Text = self.Text or node:GetText()
|
||||
mats.Parent = self.Parent or node
|
||||
mats:SetPathID( node:GetPathID() )
|
||||
mats:SetIcon( node:GetIcon() )
|
||||
mats.offset = offset
|
||||
mats.OnNodeSelected = function( mats_self, mats_node )
|
||||
OnSndNodeSelected( mats_self, mats_node, mats_self.Text, mats_node:GetFolder(), mats_node:GetPathID(), mats_node:GetIcon(), ViewPanel, pnlContent )
|
||||
end
|
||||
end
|
||||
node.Done = true
|
||||
break end
|
||||
if ( k <= offset ) then continue end
|
||||
|
||||
local p = Path .. "/"
|
||||
if ( string.StartWith( path, "addons/" ) or string.StartWith( path, "download/" ) ) then
|
||||
p = string.sub( p, string.find( p, "/sound/" ) + 1 )
|
||||
end
|
||||
|
||||
p = string.sub( p .. v, 7 )
|
||||
|
||||
spawnmenu.CreateContentIcon( "sound", ViewPanel, { spawnname = p, nicename = string.Trim( v ) } )
|
||||
|
||||
end
|
||||
|
||||
pnlContent:SwitchPanel( ViewPanel )
|
||||
|
||||
end
|
||||
|
||||
local function AddBrowseContentSnd( node, name, icon, path, pathid )
|
||||
|
||||
local ViewPanel = node.ViewPanel
|
||||
local pnlContent = node.pnlContent
|
||||
|
||||
if ( !string.EndsWith( path, "/" ) && string.len( path ) > 1 ) then path = path .. "/" end
|
||||
|
||||
local fi, fo = file.Find( path .. "sound", pathid )
|
||||
if ( !fo && !fi ) then return end
|
||||
|
||||
local sounds = node:AddFolder( name, path .. "sound", pathid, false, false, "*.*" )
|
||||
sounds:SetIcon( icon )
|
||||
|
||||
sounds.OnNodeSelected = function( self, node_sel )
|
||||
OnSndNodeSelected( self, node_sel, name, path, pathid, icon, ViewPanel, pnlContent )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
language.Add( "spawnmenu.category.browsesounds", "Browse Sounds" )
|
||||
|
||||
local function RefreshAddonSounds( browseAddonSounds )
|
||||
for _, addon in SortedPairsByMemberValue( engine.GetAddons(), "title" ) do
|
||||
|
||||
if ( !addon.downloaded ) then continue end
|
||||
if ( !addon.mounted ) then continue end
|
||||
if ( !table.HasValue( select( 2, file.Find( "*", addon.title ) ), "sound" ) ) then continue end
|
||||
|
||||
AddBrowseContentSnd( browseAddonSounds, addon.title, "icon16/bricks.png", "", addon.title )
|
||||
end
|
||||
end
|
||||
local function RefreshGameSounds( browseGameSounds )
|
||||
local games = getGameList()
|
||||
|
||||
for _, game in SortedPairsByMemberValue( games, "title" ) do
|
||||
if ( !game.mounted ) then continue end
|
||||
AddBrowseContentSnd( browseGameSounds, game.title, "games/16/" .. ( game.icon or game.folder ) .. ".png", "", game.folder )
|
||||
end
|
||||
end
|
||||
|
||||
local browseGameSounds
|
||||
local browseAddonSounds
|
||||
hook.Add( "PopulateContent", "SpawnmenuLoadSomeSounds", function( pnlContent, tree, browseNode ) timer.Simple( 0.5, function()
|
||||
|
||||
if ( !IsValid( tree ) or !IsValid( pnlContent ) ) then
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR SOUNDS !!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR SOUNDS !!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR SOUNDS !!!" )
|
||||
return
|
||||
end
|
||||
|
||||
local ViewPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
ViewPanel:SetVisible( false )
|
||||
|
||||
local browseSounds = tree:AddNode( "#spawnmenu.category.browsesounds", "icon16/sound.png" )
|
||||
browseSounds.ViewPanel = ViewPanel
|
||||
browseSounds.pnlContent = pnlContent
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
browseAddonSounds = browseSounds:AddNode( "#spawnmenu.category.addons", "icon16/folder_database.png" )
|
||||
browseAddonSounds.ViewPanel = ViewPanel
|
||||
browseAddonSounds.pnlContent = pnlContent
|
||||
|
||||
RefreshAddonSounds( browseAddonSounds )
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
local addon_sounds = {}
|
||||
local _, snd_folders = file.Find( "addons/*", "MOD" )
|
||||
for _, addon in SortedPairs( snd_folders ) do
|
||||
|
||||
if ( !file.IsDir( "addons/" .. addon .. "/sound/", "MOD" ) ) then continue end
|
||||
|
||||
table.insert( addon_sounds, addon )
|
||||
|
||||
end
|
||||
|
||||
local browseLegacySounds = browseSounds:AddNode( "#spawnmenu.category.addonslegacy", "icon16/folder_database.png" )
|
||||
browseLegacySounds.ViewPanel = ViewPanel
|
||||
browseLegacySounds.pnlContent = pnlContent
|
||||
|
||||
for _, addon in SortedPairsByValue( addon_sounds ) do
|
||||
|
||||
AddBrowseContentSnd( browseLegacySounds, addon, "icon16/bricks.png", "addons/" .. addon .. "/", "MOD" )
|
||||
|
||||
end
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
AddBrowseContentSnd( browseSounds, "#spawnmenu.category.downloads", "icon16/folder_database.png", "download/", "MOD" )
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
browseGameSounds = browseSounds:AddNode( "#spawnmenu.category.games", "icon16/folder_database.png" )
|
||||
browseGameSounds.ViewPanel = ViewPanel
|
||||
browseGameSounds.pnlContent = pnlContent
|
||||
|
||||
RefreshGameSounds( browseGameSounds )
|
||||
|
||||
end ) end )
|
||||
|
||||
hook.Add( "GameContentChanged", "ES_RefreshSpawnmenuSounds", function()
|
||||
|
||||
if ( IsValid( browseAddonSounds ) ) then
|
||||
|
||||
-- TODO: Maybe be more advaced and do not delete => recreate all the nodes, only delete nodes for addons that were removed, add only the new ones?
|
||||
browseAddonSounds:Clear()
|
||||
browseAddonSounds.ViewPanel:Clear( true )
|
||||
|
||||
RefreshAddonSounds( browseAddonSounds )
|
||||
|
||||
end
|
||||
|
||||
if ( IsValid( browseGameSounds ) ) then
|
||||
|
||||
-- TODO: Maybe be more advaced and do not delete => recreate all the nodes, only delete nodes for addons that were removed, add only the new ones?
|
||||
browseGameSounds:Clear()
|
||||
browseGameSounds.ViewPanel:Clear( true )
|
||||
|
||||
RefreshGameSounds( browseGameSounds )
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
|
||||
local function IsMaterialUsableOnEntities( matPath )
|
||||
-- A png file? No thanks
|
||||
if ( string.GetExtensionFromFilename( matPath ) ) then return false end
|
||||
|
||||
local mat = Material( matPath )
|
||||
if ( !string.find( mat:GetShader(), "LightmappedGeneric" )
|
||||
&& !string.find( mat:GetShader(), "WorldVertexTransition" )
|
||||
&& !string.find( mat:GetShader(), "Spritecard" )
|
||||
&& !string.find( mat:GetShader(), "Water" )
|
||||
&& !string.find( mat:GetShader(), "Cable" )
|
||||
--&& !string.find( mat:GetShader(), "UnlitGeneric" )
|
||||
&& !string.find( mat:GetShader(), "Refract" ) ) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local DisplayedWarning = false
|
||||
local function DisplayOneTimeWarning()
|
||||
if ( DisplayedWarning ) then return end
|
||||
DisplayedWarning = true
|
||||
|
||||
Derma_Message( "Please note that not all materials are usable on entities, such as map textures, etc.\nYou can still try though!", "Warning", "OK" )
|
||||
end
|
||||
|
||||
spawnmenu.AddContentType( "material", function( container, obj )
|
||||
if ( !obj.nicename ) then return end
|
||||
if ( !obj.spawnname ) then return end
|
||||
|
||||
local icon = vgui.Create( "ContentIcon", container )
|
||||
icon:SetContentType( "material" )
|
||||
icon:SetSpawnName( obj.spawnname )
|
||||
icon:SetName( obj.nicename )
|
||||
if ( string.GetExtensionFromFilename( obj.spawnname ) == "png" ) then
|
||||
icon:SetMaterial( obj.spawnname )
|
||||
else
|
||||
icon.Image:SetImage( obj.spawnname )
|
||||
end
|
||||
|
||||
icon.DoClick = function()
|
||||
if ( !IsMaterialUsableOnEntities( obj.spawnname ) ) then DisplayOneTimeWarning() end
|
||||
|
||||
RunConsoleCommand( "material_override", obj.spawnname )
|
||||
spawnmenu.ActivateTool( "material" )
|
||||
surface.PlaySound( "garrysmod/ui_click.wav" )
|
||||
end
|
||||
|
||||
icon.OpenMenu = function( icn )
|
||||
local menu = DermaMenu()
|
||||
menu:AddOption( "#spawnmenu.menu.copy", function() SetClipboardText( obj.spawnname ) end ):SetIcon( "icon16/page_copy.png" )
|
||||
|
||||
local str = "Use with Material Tool"
|
||||
if ( !IsMaterialUsableOnEntities( obj.spawnname ) ) then
|
||||
str = "Try to use with Material Tool (Probably won't work)"
|
||||
end
|
||||
menu:AddOption( str, function()
|
||||
RunConsoleCommand( "material_override", obj.spawnname )
|
||||
spawnmenu.ActivateTool( "material" )
|
||||
end ):SetIcon( "icon16/pencil.png" )
|
||||
|
||||
menu:AddSpacer()
|
||||
menu:AddOption( "#spawnmenu.menu.delete", function() icn:Remove() hook.Run( "SpawnlistContentChanged", icn ) end ):SetIcon( "icon16/bin_closed.png" )
|
||||
menu:Open()
|
||||
end
|
||||
|
||||
if ( IsValid( container ) ) then
|
||||
container:Add( icon )
|
||||
end
|
||||
|
||||
return icon
|
||||
|
||||
end )
|
||||
|
||||
local function OnMatNodeSelected( self, node, name, path, pathid, icon, ViewPanel, pnlContent )
|
||||
|
||||
ViewPanel:Clear( true )
|
||||
|
||||
local Path = node:GetFolder()
|
||||
|
||||
local mat_files = file.Find( Path .. "/*.vmt", node:GetPathID() )
|
||||
mat_files = table.Add( mat_files, file.Find( Path .. "/*.png", node:GetPathID() ) )
|
||||
|
||||
local offset = 0
|
||||
local limit = 512
|
||||
if ( node.offset ) then offset = node.offset or 0 end
|
||||
|
||||
for k, v in pairs( mat_files ) do
|
||||
if ( k > limit + offset ) then
|
||||
if ( !node.Done ) then
|
||||
offset = offset + limit
|
||||
local mats = ( self.Parent or node ):AddNode( ( self.Text or node:GetText() ) .. " (" .. offset .. " - " .. offset + limit .. ")" )
|
||||
mats:SetFolder( node:GetFolder() )
|
||||
mats.Text = self.Text or node:GetText()
|
||||
mats.Parent = self.Parent or node
|
||||
mats:SetPathID( node:GetPathID() )
|
||||
mats:SetIcon( node:GetIcon() )
|
||||
mats.offset = offset
|
||||
mats.OnNodeSelected = function( self_mats, node_sel )
|
||||
OnMatNodeSelected( self_mats, node_sel, self_mats.Text, node_sel:GetFolder(), node_sel:GetPathID(), node_sel:GetIcon(), ViewPanel, pnlContent )
|
||||
end
|
||||
end
|
||||
node.Done = true
|
||||
break end
|
||||
if ( k <= offset ) then continue end
|
||||
|
||||
local p = Path .. "/"
|
||||
if ( string.StartWith( path, "addons/" ) or string.StartWith( path, "download/" ) ) then
|
||||
p = string.sub( p, string.find( p, "/materials/" ) + 1 )
|
||||
end
|
||||
|
||||
p = string.sub( p .. v, 11 )
|
||||
|
||||
if ( string.GetExtensionFromFilename( p ) == "vmt" ) then
|
||||
p = string.StripExtension( p )
|
||||
v = string.StripExtension( v )
|
||||
end
|
||||
|
||||
if ( Material( p ):GetShader() == "Spritecard" ) then continue end
|
||||
|
||||
spawnmenu.CreateContentIcon( "material", ViewPanel, { spawnname = p, nicename = v } )
|
||||
end
|
||||
|
||||
pnlContent:SwitchPanel( ViewPanel )
|
||||
|
||||
end
|
||||
|
||||
local function AddBrowseContentMaterial( node, name, icon, path, pathid )
|
||||
|
||||
local ViewPanel = node.ViewPanel
|
||||
local pnlContent = node.pnlContent
|
||||
|
||||
if ( !string.EndsWith( path, "/" ) && string.len( path ) > 1 ) then path = path .. "/" end
|
||||
|
||||
local fi, fo = file.Find( path .. "materials", pathid )
|
||||
if ( !fi && !fo ) then return end
|
||||
|
||||
local materials = node:AddFolder( name, path .. "materials", pathid, false, false, "*.*" )
|
||||
materials:SetIcon( icon )
|
||||
|
||||
materials.OnNodeSelected = function( self, node_sel )
|
||||
OnMatNodeSelected( self, node_sel, name, path, pathid, icon, ViewPanel, pnlContent )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
language.Add( "spawnmenu.category.browsematerials", "Browse Materials" )
|
||||
|
||||
local function RefreshAddonMaterials( node )
|
||||
for _, addon in SortedPairsByMemberValue( engine.GetAddons(), "title" ) do
|
||||
|
||||
if ( !addon.downloaded ) then continue end
|
||||
if ( !addon.mounted ) then continue end
|
||||
if ( !table.HasValue( select( 2, file.Find( "*", addon.title ) ), "materials" ) ) then continue end
|
||||
|
||||
AddBrowseContentMaterial( node, addon.title, "icon16/bricks.png", "", addon.title )
|
||||
|
||||
end
|
||||
end
|
||||
local function RefreshGameMaterials( node )
|
||||
local games = getGameList()
|
||||
|
||||
for _, game in SortedPairsByMemberValue( games, "title" ) do
|
||||
if ( !game.mounted ) then continue end
|
||||
AddBrowseContentMaterial( node, game.title, "games/16/" .. ( game.icon or game.folder ) .. ".png", "", game.folder )
|
||||
end
|
||||
end
|
||||
|
||||
local browseAddonMaterials
|
||||
local browseGameMaterials
|
||||
hook.Add( "PopulateContent", "SpawnmenuLoadSomeMaterials", function( pnlContent, tree, browseNode ) timer.Simple( 0.5, function()
|
||||
|
||||
if ( !IsValid( tree ) or !IsValid( pnlContent ) ) then
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR MATERIALS!!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR MATERIALS!!!" )
|
||||
print( "!!! Extended Spawnmenu: FAILED TO INITALIZE PopulateContent HOOK FOR MATERIALS!!!" )
|
||||
return
|
||||
end
|
||||
|
||||
local ViewPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
ViewPanel:SetVisible( false )
|
||||
|
||||
local browseMaterials = tree:AddNode( "#spawnmenu.category.browsematerials", "icon16/picture_empty.png" )
|
||||
browseMaterials.ViewPanel = ViewPanel
|
||||
browseMaterials.pnlContent = pnlContent
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
browseAddonMaterials = browseMaterials:AddNode( "#spawnmenu.category.addons", "icon16/folder_database.png" )
|
||||
browseAddonMaterials.ViewPanel = ViewPanel
|
||||
browseAddonMaterials.pnlContent = pnlContent
|
||||
|
||||
RefreshAddonMaterials( browseAddonMaterials )
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
local addon_mats = {}
|
||||
local _, mat_folders = file.Find( "addons/*", "MOD" )
|
||||
for _, addon in SortedPairs( mat_folders ) do
|
||||
|
||||
if ( !file.IsDir( "addons/" .. addon .. "/materials/", "MOD" ) ) then continue end
|
||||
|
||||
table.insert( addon_mats, addon )
|
||||
|
||||
end
|
||||
|
||||
local browseLegacyMaterials = browseMaterials:AddNode( "#spawnmenu.category.addonslegacy", "icon16/folder_database.png" )
|
||||
browseLegacyMaterials.ViewPanel = ViewPanel
|
||||
browseLegacyMaterials.pnlContent = pnlContent
|
||||
|
||||
for _, addon in SortedPairsByValue( addon_mats ) do
|
||||
|
||||
AddBrowseContentMaterial( browseLegacyMaterials, addon, "icon16/bricks.png", "addons/" .. addon .. "/", "MOD" )
|
||||
|
||||
end
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
AddBrowseContentMaterial( browseMaterials, "#spawnmenu.category.downloads", "icon16/folder_database.png", "download/", "MOD" )
|
||||
|
||||
--[[ --------------------------------------------------------------------------------------- ]]
|
||||
|
||||
browseGameMaterials = browseMaterials:AddNode( "#spawnmenu.category.games", "icon16/folder_database.png" )
|
||||
browseGameMaterials.ViewPanel = ViewPanel
|
||||
browseGameMaterials.pnlContent = pnlContent
|
||||
|
||||
RefreshGameMaterials( browseGameMaterials )
|
||||
|
||||
end ) end )
|
||||
|
||||
hook.Add( "GameContentChanged", "ES_RefreshSpawnmenuMaterials", function()
|
||||
|
||||
if ( IsValid( browseAddonMaterials ) ) then
|
||||
|
||||
-- TODO: Maybe be more advaced and do not delete => recreate all the nodes, only delete nodes for addons that were removed, add only the new ones?
|
||||
browseAddonMaterials:Clear()
|
||||
browseAddonMaterials.ViewPanel:Clear( true )
|
||||
|
||||
RefreshAddonMaterials( browseAddonMaterials )
|
||||
|
||||
end
|
||||
|
||||
if ( IsValid( browseGameMaterials ) ) then
|
||||
|
||||
-- TODO: Maybe be more advaced and do not delete => recreate all the nodes, only delete nodes for addons that were removed, add only the new ones?
|
||||
browseGameMaterials:Clear()
|
||||
browseGameMaterials.ViewPanel:Clear( true )
|
||||
|
||||
RefreshGameMaterials( browseGameMaterials )
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
--[[ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ]]
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu_entities", function( pnlContent, tree, node )
|
||||
if ( !cl_addTabs:GetBool() ) then return end
|
||||
|
||||
local node_w = tree:AddNode( "#spawnmenu.category.entities", "icon16/bricks.png" )
|
||||
|
||||
node_w.PropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
node_w.PropPanel:SetVisible( false )
|
||||
|
||||
function node_w:DoClick()
|
||||
pnlContent:SwitchPanel( self.PropPanel )
|
||||
end
|
||||
|
||||
local Categorised = {}
|
||||
|
||||
local SpawnableEntities = list.Get( "SpawnableEntities" )
|
||||
if ( SpawnableEntities ) then
|
||||
for k, v in pairs( SpawnableEntities ) do
|
||||
v.Category = v.Category or "Other"
|
||||
Categorised[ v.Category ] = Categorised[ v.Category ] or {}
|
||||
table.insert( Categorised[ v.Category ], v )
|
||||
end
|
||||
end
|
||||
|
||||
for CategoryName, v in SortedPairs( Categorised ) do
|
||||
|
||||
local node_new = node_w:AddNode( CategoryName, "icon16/bricks.png" )
|
||||
|
||||
local CatPropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
CatPropPanel:SetVisible( false )
|
||||
|
||||
local Header = vgui.Create("ContentHeader", node_w.PropPanel )
|
||||
Header:SetText( CategoryName )
|
||||
node_w.PropPanel:Add( Header )
|
||||
|
||||
for k, ent in SortedPairsByMemberValue( v, "PrintName" ) do
|
||||
local t = {
|
||||
nicename = ent.PrintName or ent.ClassName,
|
||||
spawnname = ent.ClassName,
|
||||
material = "entities/" .. ent.ClassName .. ".png",
|
||||
admin = ent.AdminOnly
|
||||
}
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "entity", CatPropPanel, t )
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "entity", node_w.PropPanel, t )
|
||||
end
|
||||
|
||||
function node_new:DoClick()
|
||||
pnlContent:SwitchPanel( CatPropPanel )
|
||||
end
|
||||
|
||||
end
|
||||
end )
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu_post_processing", function( pnlContent, tree, node )
|
||||
if ( !cl_addTabs:GetBool() ) then return end
|
||||
|
||||
local node_w = tree:AddNode( "#spawnmenu.category.postprocess", "icon16/picture.png" )
|
||||
|
||||
node_w.PropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
node_w.PropPanel:SetVisible( false )
|
||||
|
||||
function node_w:DoClick()
|
||||
pnlContent:SwitchPanel( self.PropPanel )
|
||||
end
|
||||
|
||||
-- Get the table
|
||||
|
||||
local Categorised = {}
|
||||
local PostProcess = list.Get( "PostProcess" )
|
||||
|
||||
if ( PostProcess ) then
|
||||
for k, v in pairs( PostProcess ) do
|
||||
v.category = v.category or "Other"
|
||||
v.name = k
|
||||
Categorised[ v.category ] = Categorised[ v.category ] or {}
|
||||
table.insert( Categorised[ v.category ], v )
|
||||
end
|
||||
end
|
||||
|
||||
-- Put table into panels
|
||||
for CategoryName, v in SortedPairs( Categorised ) do
|
||||
|
||||
local node_new = node_w:AddNode( CategoryName, "icon16/picture.png" )
|
||||
|
||||
local CatPropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
CatPropPanel:SetVisible( false )
|
||||
|
||||
local Header = vgui.Create( "ContentHeader", node_w.PropPanel )
|
||||
Header:SetText( CategoryName )
|
||||
node_w.PropPanel:Add( Header )
|
||||
|
||||
for k, pp in SortedPairsByMemberValue( v, "PrintName" ) do
|
||||
if ( pp.func ) then pp.func( CatPropPanel ) pp.func( node_w.PropPanel ) continue end
|
||||
|
||||
local t = {
|
||||
name = pp.name,
|
||||
icon = pp.icon
|
||||
}
|
||||
|
||||
spawnmenu.CreateContentIcon( "postprocess", CatPropPanel, t )
|
||||
spawnmenu.CreateContentIcon( "postprocess", node_w.PropPanel, t )
|
||||
end
|
||||
|
||||
function node_new:DoClick()
|
||||
pnlContent:SwitchPanel( CatPropPanel )
|
||||
end
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu_npcs", function( pnlContent, tree, node )
|
||||
if ( !cl_addTabs:GetBool() ) then return end
|
||||
|
||||
local node_w = tree:AddNode( "#spawnmenu.category.npcs", "icon16/monkey.png" )
|
||||
|
||||
node_w.PropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
node_w.PropPanel:SetVisible( false )
|
||||
|
||||
function node_w:DoClick()
|
||||
pnlContent:SwitchPanel( self.PropPanel )
|
||||
end
|
||||
|
||||
local NPCList = list.Get( "NPC" )
|
||||
local Categories = {}
|
||||
|
||||
for k, v in pairs( NPCList ) do
|
||||
local Category = v.Category or "Other"
|
||||
local Tab = Categories[ Category ] or {}
|
||||
|
||||
Tab[ k ] = v
|
||||
|
||||
Categories[ Category ] = Tab
|
||||
end
|
||||
|
||||
for CategoryName, v in SortedPairs( Categories ) do
|
||||
|
||||
local node_new = node_w:AddNode( CategoryName, "icon16/monkey.png" )
|
||||
|
||||
local CatPropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
CatPropPanel:SetVisible( false )
|
||||
|
||||
local Header = vgui.Create("ContentHeader", node_w.PropPanel )
|
||||
Header:SetText( CategoryName )
|
||||
node_w.PropPanel:Add( Header )
|
||||
|
||||
for name, ent in SortedPairsByMemberValue( v, "Name" ) do
|
||||
local t = {
|
||||
nicename = ent.Name or name,
|
||||
spawnname = name,
|
||||
material = "entities/" .. name .. ".png",
|
||||
weapon = ent.Weapons,
|
||||
admin = ent.AdminOnly
|
||||
}
|
||||
spawnmenu.CreateContentIcon( "npc", CatPropPanel, t )
|
||||
spawnmenu.CreateContentIcon( "npc", node_w.PropPanel, t )
|
||||
end
|
||||
|
||||
function node_new:DoClick()
|
||||
pnlContent:SwitchPanel( CatPropPanel )
|
||||
end
|
||||
|
||||
end
|
||||
end )
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu_vehicles", function( pnlContent, tree, node )
|
||||
if ( !cl_addTabs:GetBool() ) then return end
|
||||
|
||||
local node_w = tree:AddNode( "#spawnmenu.category.vehicles", "icon16/car.png" )
|
||||
|
||||
node_w.PropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
node_w.PropPanel:SetVisible( false )
|
||||
|
||||
function node_w:DoClick()
|
||||
pnlContent:SwitchPanel( self.PropPanel )
|
||||
end
|
||||
|
||||
local Categorised = {}
|
||||
local Vehicles = list.Get( "Vehicles" )
|
||||
if ( Vehicles ) then
|
||||
for k, v in pairs( Vehicles ) do
|
||||
v.Category = v.Category or "Other"
|
||||
Categorised[ v.Category ] = Categorised[ v.Category ] or {}
|
||||
v.ClassName = k
|
||||
v.PrintName = v.Name
|
||||
v.ScriptedEntityType = "vehicle"
|
||||
table.insert( Categorised[ v.Category ], v )
|
||||
end
|
||||
end
|
||||
|
||||
for CategoryName, v in SortedPairs( Categorised ) do
|
||||
|
||||
local node_new = node_w:AddNode( CategoryName, "icon16/car.png" )
|
||||
|
||||
local CatPropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
CatPropPanel:SetVisible( false )
|
||||
|
||||
local Header = vgui.Create("ContentHeader", node_w.PropPanel )
|
||||
Header:SetText( CategoryName )
|
||||
node_w.PropPanel:Add( Header )
|
||||
|
||||
for k, ent in SortedPairsByMemberValue( v, "PrintName" ) do
|
||||
local t = {
|
||||
nicename = ent.PrintName or ent.ClassName,
|
||||
spawnname = ent.ClassName,
|
||||
material = "entities/" .. ent.ClassName .. ".png",
|
||||
admin = ent.AdminOnly
|
||||
}
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "entity", node_w.PropPanel, t )
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "entity", CatPropPanel, t )
|
||||
end
|
||||
|
||||
function node_new:DoClick()
|
||||
pnlContent:SwitchPanel( CatPropPanel )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
|
||||
hook.Add( "PopulateContent", "rb655_extended_spawnmenu_weapons", function( pnlContent, tree, node )
|
||||
if ( !cl_addTabs:GetBool() ) then return end
|
||||
|
||||
local node_w = tree:AddNode( "#spawnmenu.category.weapons", "icon16/gun.png" )
|
||||
|
||||
node_w.PropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
node_w.PropPanel:SetVisible( false )
|
||||
|
||||
function node_w:DoClick()
|
||||
pnlContent:SwitchPanel( self.PropPanel )
|
||||
end
|
||||
|
||||
local Weapons = list.Get( "Weapon" )
|
||||
local Categorised = {}
|
||||
|
||||
for k, weapon in pairs( Weapons ) do
|
||||
if ( !weapon.Spawnable && !weapon.AdminSpawnable ) then continue end
|
||||
|
||||
Categorised[ weapon.Category ] = Categorised[ weapon.Category ] or {}
|
||||
table.insert( Categorised[ weapon.Category ], weapon )
|
||||
end
|
||||
|
||||
for CategoryName, v in SortedPairs( Categorised ) do
|
||||
local node_new = node_w:AddNode( CategoryName, "icon16/gun.png" )
|
||||
|
||||
local CatPropPanel = vgui.Create( "ContentContainer", pnlContent )
|
||||
CatPropPanel:SetVisible( false )
|
||||
|
||||
local Header = vgui.Create("ContentHeader", node_w.PropPanel )
|
||||
Header:SetText( CategoryName )
|
||||
node_w.PropPanel:Add( Header )
|
||||
|
||||
for k, ent in SortedPairsByMemberValue( v, "PrintName" ) do
|
||||
local t = {
|
||||
nicename = ent.PrintName or ent.ClassName,
|
||||
spawnname = ent.ClassName,
|
||||
material = "entities/" .. ent.ClassName .. ".png",
|
||||
admin = ent.AdminOnly
|
||||
}
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "weapon", CatPropPanel, t )
|
||||
spawnmenu.CreateContentIcon( ent.ScriptedEntityType or "weapon", node_w.PropPanel, t )
|
||||
end
|
||||
|
||||
function node_new:DoClick()
|
||||
pnlContent:SwitchPanel( CatPropPanel )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end )
|
||||
@@ -0,0 +1,39 @@
|
||||
PLUGIN.name = "Extended Spawnmenu"
|
||||
PLUGIN.author = "RB655 (Портирован для Helix)"
|
||||
PLUGIN.description = "Расширенное спавн-меню с поддержкой звуков и legacy аддонов"
|
||||
|
||||
ix.util.Include("cl_spawnmenu.lua")
|
||||
ix.util.Include("cl_legacy.lua")
|
||||
-- Конфигурация
|
||||
ix.config.Add("extSpawnmenuEnabled", true, "Включить расширенное спавн-меню", nil, {
|
||||
category = "Extended Spawnmenu"
|
||||
})
|
||||
|
||||
ix.config.Add("extSpawnmenuCreateTabs", false, "Создавать отдельные вкладки для категорий", nil, {
|
||||
category = "Extended Spawnmenu"
|
||||
})
|
||||
|
||||
-- Серверная часть
|
||||
if SERVER then
|
||||
util.AddNetworkString("rb655_playsound")
|
||||
|
||||
-- Команда для проигрывания звуков всем игрокам
|
||||
concommand.Add("rb655_playsound_all", function(ply, cmd, args)
|
||||
if not ply:IsSuperAdmin() or not args[1] or string.Trim(args[1]) == "" then return end
|
||||
|
||||
net.Start("rb655_playsound")
|
||||
net.WriteString(args[1] or "")
|
||||
net.Broadcast()
|
||||
end)
|
||||
end
|
||||
|
||||
-- Клиентская часть
|
||||
if CLIENT then
|
||||
-- ConVar для совместимости
|
||||
CreateClientConVar("rb655_create_sm_tabs", "0", true, true)
|
||||
end
|
||||
|
||||
function PLUGIN:Initialize()
|
||||
print("[Extended Spawnmenu] Расширенное спавн-меню загружено!")
|
||||
print("[Extended Spawnmenu] Добавлены: Browse Sounds, Materials, Legacy Addons")
|
||||
end
|
||||
2709
garrysmod/gamemodes/militaryrp/plugins/f4menu/cl_plugin.lua
Normal file
2709
garrysmod/gamemodes/militaryrp/plugins/f4menu/cl_plugin.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,253 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.donateCatalog = PLUGIN.donateCatalog or {
|
||||
{
|
||||
id = "currency",
|
||||
name = "Рубли",
|
||||
tagline = "Пополнение игрового баланса",
|
||||
accent = { r = 1, g = 104, b = 44 },
|
||||
items = {
|
||||
{
|
||||
id = "money_1000",
|
||||
title = "1.000 ₽",
|
||||
price = 50,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Пополнение игрового баланса",
|
||||
"Мгновенное зачисление",
|
||||
"Использование в магазинах"
|
||||
},
|
||||
reward = {
|
||||
type = "money",
|
||||
amount = 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "money_5000",
|
||||
title = "5.000 ₽",
|
||||
price = 225,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Пополнение игрового баланса",
|
||||
"Мгновенное зачисление",
|
||||
"Использование в магазинах",
|
||||
"Выгодно: +10% бонус"
|
||||
},
|
||||
tag = "ВЫГОДНО",
|
||||
reward = {
|
||||
type = "money",
|
||||
amount = 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "money_10000",
|
||||
title = "10.000 ₽",
|
||||
price = 400,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Пополнение игрового баланса",
|
||||
"Мгновенное зачисление",
|
||||
"Использование в магазинах",
|
||||
"Выгодно: +20% бонус"
|
||||
},
|
||||
tag = "ХИТ",
|
||||
reward = {
|
||||
type = "money",
|
||||
amount = 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "money_25000",
|
||||
title = "25.000 ₽",
|
||||
price = 900,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Пополнение игрового баланса",
|
||||
"Мгновенное зачисление",
|
||||
"Использование в магазинах",
|
||||
"Максимально выгодно: +28% бонус"
|
||||
},
|
||||
tag = "ЛУЧШЕЕ",
|
||||
reward = {
|
||||
type = "money",
|
||||
amount = 25000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "weapons",
|
||||
name = "Оружие",
|
||||
tagline = "Донатное оружие в ваш арсенал",
|
||||
accent = { r = 1, g = 104, b = 44 },
|
||||
items = {
|
||||
{ id = "ak12", title = "AK-12", price1Month = 1200, price3Month = 3200, currency = "IGS", perks = { "Доступен в арсенале", "Кулдаун 10 минут", "Любая фракция" }, reward = { type = "weapon", weaponClass = "tacrp_ak_ak12", name = "AK-12" } },
|
||||
{ id = "mdr", title = "Desert Tech MDR", price1Month = 1500, price3Month = 4000, currency = "IGS", perks = { "Доступен в арсенале", "Кулдаун 10 минут", "Любая фракция" }, reward = { type = "weapon", weaponClass = "arc9_eft_mdr", name = "MDR" } },
|
||||
{ id = "mp7a1", title = "MP7A1", price1Month = 1300, price3Month = 3500, currency = "IGS", perks = { "Доступен в арсенале", "Кулдаун 10 минут", "Любая фракция" }, reward = { type = "weapon", weaponClass = "arc9_eft_mp7a1", name = "MP7A1" } },
|
||||
{ id = "ai_axmc", title = "AI AXMC", price1Month = 2000, price3Month = 5500, currency = "IGS", perks = { "Снайперская винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_ai_axmc", name = "AI AXMC" } },
|
||||
{ id = "t5000", title = "T-5000", price1Month = 1900, price3Month = 5200, currency = "IGS", perks = { "Снайперская винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_t5000", name = "T-5000" } },
|
||||
{ id = "saiga12k", title = "Сайга-12К", price1Month = 1400, price3Month = 3800, currency = "IGS", perks = { "Автоматический дробовик", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_saiga12k", name = "Сайга-12К" } },
|
||||
{ id = "deagle", title = "Desert Eagle XIX", price1Month = 800, price3Month = 2200, currency = "IGS", perks = { "Мощный пистолет", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_deagle_xix", name = "Desert Eagle" } },
|
||||
{ id = "apb", title = "АПБ", price1Month = 700, price3Month = 1900, currency = "IGS", perks = { "Бесшумный пистолет", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_apb", name = "АПБ" } },
|
||||
{ id = "spear", title = "SPEAR", price1Month = 1600, price3Month = 4400, currency = "IGS", perks = { "Доступен в арсенале", "Кулдаун 10 минут", "Любая фракция" }, reward = { type = "weapon", weaponClass = "arc9_eft_spear", name = "SPEAR" } },
|
||||
{ id = "sr25", title = "SR-25", price1Month = 1800, price3Month = 4900, currency = "IGS", perks = { "Снайперская винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_sr25", name = "SR-25" } },
|
||||
{ id = "dvl10", title = "ДВЛ-10", price1Month = 1900, price3Month = 5200, currency = "IGS", perks = { "Снайперская винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_dvl10", name = "ДВЛ-10" } },
|
||||
{ id = "hultafors", title = "Dead Blow Hammer", price1Month = 300, price3Month = 800, currency = "IGS", perks = { "Холодное оружие", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_melee_hultafors", name = "Hammer" } },
|
||||
{ id = "cultist", title = "Cultist Knife", price1Month = 400, price3Month = 1100, currency = "IGS", perks = { "Холодное оружие", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_melee_cultist", name = "Cultist Knife" } },
|
||||
{ id = "akula", title = "Akula", price1Month = 350, price3Month = 950, currency = "IGS", perks = { "Холодное оружие", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_melee_akula", name = "Akula" } },
|
||||
{ id = "crash", title = "Crash Axe", price1Month = 350, price3Month = 950, currency = "IGS", perks = { "Холодное оружие", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_melee_crash", name = "Crash Axe" } },
|
||||
{ id = "kukri", title = "Kukri", price1Month = 400, price3Month = 1100, currency = "IGS", perks = { "Холодное оружие", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_melee_kukri", name = "Kukri" } },
|
||||
{ id = "ak50", title = "AK-50", price1Month = 2200, price3Month = 6000, currency = "IGS", perks = { "Мощная штурмовая винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, tag = "ХИТ", reward = { type = "weapon", weaponClass = "arc9_eft_ak50", name = "AK-50" } },
|
||||
{ id = "vss", title = "ВСС Винторез", price1Month = 1500, price3Month = 4100, currency = "IGS", perks = { "Бесшумная винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_vss", name = "ВСС" } },
|
||||
{ id = "mr43", title = "МР-43", price1Month = 1000, price3Month = 2700, currency = "IGS", perks = { "Дробовик", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_mr43", name = "МР-43" } },
|
||||
{ id = "rpk16", title = "РПК-16", price1Month = 1700, price3Month = 4600, currency = "IGS", perks = { "Ручной пулемёт", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_rpk16", name = "РПК-16" } },
|
||||
{ id = "rshg2", title = "РШГ-2", price1Month = 2500, price3Month = 6800, currency = "IGS", perks = { "Гранатомёт", "Доступен в арсенале", "Кулдаун 10 минут" }, tag = "РЕДКОЕ", reward = { type = "weapon", weaponClass = "arc9_eft_rshg2", name = "РШГ-2" } },
|
||||
{ id = "ash12", title = "АШ-12", price1Month = 1800, price3Month = 4900, currency = "IGS", perks = { "Штурмовая винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_ash12", name = "АШ-12" } },
|
||||
{ id = "rd704", title = "RD-704", price1Month = 1400, price3Month = 3800, currency = "IGS", perks = { "Штурмовая винтовка", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_rd704", name = "RD-704" } },
|
||||
{ id = "ppsh41", title = "ППШ-41", price1Month = 900, price3Month = 2400, currency = "IGS", perks = { "Легендарный ПП", "Доступен в арсенале", "Кулдаун 10 минут" }, reward = { type = "weapon", weaponClass = "arc9_eft_ppsh41", name = "ППШ-41" } }
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "other",
|
||||
name = "Другое",
|
||||
tagline = "Дополнительные возможности",
|
||||
accent = { r = 1, g = 104, b = 44 },
|
||||
items = {
|
||||
{
|
||||
id = "voice_chat",
|
||||
title = "Говорилка",
|
||||
price = 500,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Разблокировка голосового чата",
|
||||
"Радиус слышимости 200м",
|
||||
"Без ограничений по времени",
|
||||
"Мгновенная активация"
|
||||
},
|
||||
reward = {
|
||||
type = "voice_chat",
|
||||
successMessage = "Голосовой чат разблокирован!"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "privileges",
|
||||
name = "Привелегии",
|
||||
tagline = "VIP статусы и возможности",
|
||||
accent = { r = 1, g = 104, b = 44 },
|
||||
items = {
|
||||
{
|
||||
id = "vip",
|
||||
title = "VIP",
|
||||
price1Month = 300,
|
||||
price3Month = 800,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Цветной ник и иконка VIP в чате",
|
||||
"Приоритет при входе на сервер",
|
||||
"Скидка 10% в арсенале",
|
||||
"Доступ к VIP-командам"
|
||||
},
|
||||
reward = {
|
||||
type = "privilege",
|
||||
tier = "vip"
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "vip_plus",
|
||||
title = "VIP+",
|
||||
price1Month = 500,
|
||||
price3Month = 1400,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Все преимущества VIP",
|
||||
"Увеличенная зарплата (+15%)",
|
||||
"Скидка 15% в арсенале",
|
||||
"Доступ к уникальным моделям",
|
||||
"Еженедельный бонус валюты"
|
||||
},
|
||||
reward = {
|
||||
type = "privilege",
|
||||
tier = "vip_plus"
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "premium",
|
||||
title = "Premium",
|
||||
price1Month = 800,
|
||||
price3Month = 2200,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Все преимущества VIP+",
|
||||
"Увеличенная зарплата (+25%)",
|
||||
"Скидка 20% в арсенале",
|
||||
"Приоритетный спавн техники",
|
||||
"Доступ к Premium-командам",
|
||||
"Уникальные анимации и эффекты"
|
||||
},
|
||||
tag = "ХИТ",
|
||||
reward = {
|
||||
type = "privilege",
|
||||
tier = "premium"
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "sponsor",
|
||||
title = "Спонсор",
|
||||
price1Month = 1300,
|
||||
price3Month = 3600,
|
||||
currency = "IGS",
|
||||
perks = {
|
||||
"Доступ к личному Discord-серверу разработчиков проекта с различной информацией, включая оповещения о свежих обновлениях",
|
||||
"Увеличенный доход за убийство противника на сервере",
|
||||
"Быстрый захват точки противника (в 1,5 раза быстрее)",
|
||||
"Уменьшенное ограничение (по времени) на выкат техники",
|
||||
"Постоянно действующие скидки в 20% на покупку доната (НЕ ЧЕРЕЗ АВТО-ДОНАТ)",
|
||||
"Уникальные возможности на сервере",
|
||||
"Участие в закрытых бета-тестированиях и голосованиях"
|
||||
},
|
||||
tag = "ЭКСКЛЮЗИВ",
|
||||
reward = {
|
||||
type = "privilege",
|
||||
tier = "sponsor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local function BuildDonateLookup()
|
||||
PLUGIN.donateItemsByID = {}
|
||||
for _, category in ipairs(PLUGIN.donateCatalog or {}) do
|
||||
for _, item in ipairs(category.items or {}) do
|
||||
PLUGIN.donateItemsByID[item.id] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
BuildDonateLookup()
|
||||
|
||||
function PLUGIN:GetDonateCatalog()
|
||||
return self.donateCatalog or {}
|
||||
end
|
||||
|
||||
function PLUGIN:GetDonateCategory(identifier)
|
||||
if not identifier then return end
|
||||
|
||||
for _, category in ipairs(self:GetDonateCatalog()) do
|
||||
if category.id == identifier then
|
||||
return category
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:GetDonateProduct(identifier)
|
||||
if not identifier then return end
|
||||
|
||||
if not self.donateItemsByID then
|
||||
BuildDonateLookup()
|
||||
end
|
||||
|
||||
return self.donateItemsByID[identifier]
|
||||
end
|
||||
36
garrysmod/gamemodes/militaryrp/plugins/f4menu/sh_plugin.lua
Normal file
36
garrysmod/gamemodes/militaryrp/plugins/f4menu/sh_plugin.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
PLUGIN.name = "F4 Menu"
|
||||
PLUGIN.author = "RefoselTeamWork"
|
||||
PLUGIN.description = "F4 Menu for Military RP"
|
||||
|
||||
-- Ссылки на социальные сети
|
||||
PLUGIN.socialLinks = {
|
||||
discord = "https://discord.gg/paQdrP7aD7",
|
||||
steam = "https://steamcommunity.com/sharedfiles/filedetails/?id=3630170114",
|
||||
tiktok = "https://www.tiktok.com/@front.team0?_t=ZS-8zQYdHdzDB1&_r=1",
|
||||
telegram = "https://t.me/frontteamproject"
|
||||
}
|
||||
|
||||
-- Материалы иконок социальных сетей
|
||||
PLUGIN.socialIcons = {
|
||||
discord = "materials/ft_ui/military/vnu/f4menu/icons/discord.png",
|
||||
steam = "materials/ft_ui/military/vnu/f4menu/icons/steam.png",
|
||||
tiktok = "materials/ft_ui/military/vnu/f4menu/icons/tiktok.png",
|
||||
telegram = "materials/ft_ui/military/vnu/f4menu/icons/telegram.png"
|
||||
}
|
||||
|
||||
ix.util.Include("sh_donate_config.lua")
|
||||
ix.util.Include("sh_thanks_config.lua")
|
||||
ix.util.Include("sv_plugin.lua")
|
||||
ix.util.Include("cl_plugin.lua")
|
||||
|
||||
ix.command.Add("Donate", {
|
||||
description = "Открыть меню доната (IGS)",
|
||||
alias = {"Donat", "Донат"},
|
||||
OnRun = function(self, client)
|
||||
if (CLIENT) then
|
||||
RunConsoleCommand("igs")
|
||||
else
|
||||
client:ConCommand("igs")
|
||||
end
|
||||
end
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
PLUGIN.thanksPages = PLUGIN.thanksPages or {
|
||||
{
|
||||
id = "founders",
|
||||
title = "Руководители",
|
||||
people = {
|
||||
{
|
||||
name = "Oleg Zakon",
|
||||
model = "models/player/gman_high.mdl",
|
||||
sequence = "pose_standing_01",
|
||||
description = "Основатель и идейный вдохновитель Front Team. Отвечает за общее развитие проекта, стратегию и ключевые решения. Следит за качеством контента, стабильностью сервера и взаимодействием с сообществом.",
|
||||
link = "https://steamcommunity.com/profiles/76561198204118180"
|
||||
},
|
||||
{
|
||||
name = "Бугор",
|
||||
model = "models/player/breen.mdl",
|
||||
sequence = "pose_standing_01",
|
||||
description = "Правая рука владельца. Курирует техническую часть, развитие игровых механик и поддержку проекта. Обеспечивает бесперебойную работу сервера и помогает в реализации идей сообщества.",
|
||||
link = "https://steamcommunity.com/profiles/76561199186141452"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "developers",
|
||||
title = "Разработчики",
|
||||
people = {
|
||||
|
||||
{
|
||||
name = "Биба",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Настройщик игровых механик",
|
||||
link = "https://steamcommunity.com/profiles/76561199189433293"
|
||||
},
|
||||
{
|
||||
name = "Scripty",
|
||||
model = "models/player/skeleton.mdl",
|
||||
sequence = "pose_standing_04",
|
||||
description = "Разработчик",
|
||||
link = "https://steamcommunity.com/profiles/76561198164572709"
|
||||
},
|
||||
{
|
||||
name = "ItzTomber",
|
||||
model = "models/player/kleiner.mdl",
|
||||
sequence = "pose_standing_04",
|
||||
description = "Разработчик",
|
||||
link = "https://steamcommunity.com/profiles/76561198341626975"
|
||||
},
|
||||
{
|
||||
name = "Hari",
|
||||
model = "models/player/magnusson.mdl",
|
||||
sequence = "pose_standing_02",
|
||||
description = "Разработчик.",
|
||||
link = "https://steamcommunity.com/profiles/76561198201651767"
|
||||
},
|
||||
{
|
||||
name = "Refosel",
|
||||
model = "models/kemono_friends/ezo_red_fox/ezo_red_fox_player.mdl",
|
||||
sequence = "pose_standing_01",
|
||||
description = "Главный Разработчик",
|
||||
link = "https://steamcommunity.com/profiles/76561198393073512"
|
||||
},
|
||||
{
|
||||
name = "Прохор",
|
||||
model = "models/player/odessa.mdl",
|
||||
sequence = "pose_standing_02",
|
||||
description = "Маппер.",
|
||||
link = "no link"
|
||||
},
|
||||
{
|
||||
name = "Сварщик",
|
||||
model = "models/player/eli.mdl",
|
||||
sequence = "pose_standing_04",
|
||||
description = "Модделер.",
|
||||
link = "https://steamcommunity.com/profiles/76561198440513870"
|
||||
},
|
||||
{
|
||||
name = "Козырный",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Figma дизайнер.",
|
||||
link = "https://steamcommunity.com/profiles/76561199077227396"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id = "helix",
|
||||
title = "Helix",
|
||||
people = {
|
||||
{
|
||||
name = "NutScript",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://github.com/NutScript/NutScript"
|
||||
},
|
||||
{
|
||||
name = "Alex Grist",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561197979205163"
|
||||
},
|
||||
{
|
||||
name = "Igor Radovanovic",
|
||||
model = "models/player/combine_soldier.mdl",
|
||||
sequence = "pose_standing_04",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561197990111113"
|
||||
},
|
||||
{
|
||||
name = "Jaydawg",
|
||||
model = "models/player/combine_soldier_prisonguard.mdl",
|
||||
sequence = "pose_standing_02",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561197970371430"
|
||||
},
|
||||
{
|
||||
name = "nebulous",
|
||||
model = "models/player/combine_super_soldier.mdl",
|
||||
sequence = "pose_standing_01",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://github.com/NebulousCloud"
|
||||
},
|
||||
{
|
||||
name = "Black Tea",
|
||||
model = "models/player/combine_soldier_prisonguard.mdl",
|
||||
sequence = "pose_standing_02",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561197999893894"
|
||||
},
|
||||
{
|
||||
name = "Rain GBizzle",
|
||||
model = "models/player/combine_soldier.mdl",
|
||||
sequence = "pose_standing_04",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561198036111376"
|
||||
},
|
||||
{
|
||||
name = "Luna",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://steamcommunity.com/profiles/76561197988658543"
|
||||
},
|
||||
{
|
||||
name = "Contributors",
|
||||
model = "models/player/police.mdl",
|
||||
sequence = "pose_ducking_01",
|
||||
description = "Спасибо за Helix.",
|
||||
link = "https://github.com/NebulousCloud/helix/graphs/contributors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PLUGIN:GetThanksPages()
|
||||
return self.thanksPages or {}
|
||||
end
|
||||
|
||||
function PLUGIN:GetThanksPage(identifier)
|
||||
if not identifier then return end
|
||||
|
||||
for _, page in ipairs(self:GetThanksPages()) do
|
||||
if page.id == identifier then
|
||||
return page
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:GetThanksPerson(pageID, personName)
|
||||
local page = self:GetThanksPage(pageID)
|
||||
if not page then return end
|
||||
|
||||
for _, person in ipairs(page.people or {}) do
|
||||
if person.name == personName then
|
||||
return person
|
||||
end
|
||||
end
|
||||
end
|
||||
667
garrysmod/gamemodes/militaryrp/plugins/f4menu/sv_plugin.lua
Normal file
667
garrysmod/gamemodes/militaryrp/plugins/f4menu/sv_plugin.lua
Normal file
@@ -0,0 +1,667 @@
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
util.AddNetworkString("ix.F4_RequestInfo")
|
||||
util.AddNetworkString("ix.F4_SendInfo")
|
||||
util.AddNetworkString("ix.F4_DonatePurchase")
|
||||
util.AddNetworkString("ix.F4_DonatePurchaseResult")
|
||||
util.AddNetworkString("ixChangeName")
|
||||
util.AddNetworkString("ixF4UpdateName")
|
||||
|
||||
local function GetFactionCounters(factionID, podIndex, specIndex)
|
||||
local factionOnline, samePod, sameSpec = 0, 0, 0
|
||||
|
||||
for _, ply in ipairs(player.GetAll()) do
|
||||
if not IsValid(ply) then continue end
|
||||
if ply:Team() ~= factionID then continue end
|
||||
|
||||
factionOnline = factionOnline + 1
|
||||
|
||||
local char = ply:GetCharacter()
|
||||
if not char then continue end
|
||||
|
||||
if char:GetPodr() == podIndex then
|
||||
samePod = samePod + 1
|
||||
end
|
||||
|
||||
if char:GetSpec() == specIndex then
|
||||
sameSpec = sameSpec + 1
|
||||
end
|
||||
end
|
||||
|
||||
return factionOnline, samePod, sameSpec
|
||||
end
|
||||
|
||||
local function BuildInfoPayload(client)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
|
||||
local factionID = char:GetFaction()
|
||||
local factionTable = ix.faction.Get(factionID)
|
||||
local podIndex = char:GetPodr()
|
||||
local specIndex = char:GetSpec()
|
||||
local rankIndex = char:GetRank() or 1
|
||||
|
||||
local factionOnline, samePod, sameSpec = GetFactionCounters(factionID, podIndex, specIndex)
|
||||
|
||||
local vehiclesPlugin = ix.plugin.Get("vehicles")
|
||||
local arsenalPlugin = ix.plugin.Get("arsenal")
|
||||
|
||||
local techPoints = 0
|
||||
if vehiclesPlugin and vehiclesPlugin.GetFactionPoints then
|
||||
techPoints = vehiclesPlugin:GetFactionPoints(factionID) or 0
|
||||
end
|
||||
|
||||
local supplyPoints = 0
|
||||
if arsenalPlugin and arsenalPlugin.GetFactionSupply then
|
||||
supplyPoints = arsenalPlugin:GetFactionSupply(factionID) or 0
|
||||
end
|
||||
|
||||
local activeVehicles = 0
|
||||
if vehiclesPlugin and vehiclesPlugin.activeVehicles then
|
||||
for _, data in pairs(vehiclesPlugin.activeVehicles) do
|
||||
if not istable(data) then continue end
|
||||
if data.faction == factionID then
|
||||
activeVehicles = activeVehicles + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local subdivisionData = {
|
||||
index = podIndex,
|
||||
name = "Не назначено",
|
||||
preset = {},
|
||||
specDefault = 1,
|
||||
members = samePod,
|
||||
model = "",
|
||||
skin = 0,
|
||||
bodygroups = {}
|
||||
}
|
||||
|
||||
local specData = {
|
||||
index = specIndex,
|
||||
name = "Не назначено",
|
||||
weapons = {},
|
||||
members = sameSpec,
|
||||
podr = 0
|
||||
}
|
||||
|
||||
local rankData = {
|
||||
index = rankIndex,
|
||||
name = "Без звания"
|
||||
}
|
||||
|
||||
if factionTable then
|
||||
if factionTable.Podr and factionTable.Podr[podIndex] then
|
||||
local unit = factionTable.Podr[podIndex]
|
||||
subdivisionData.name = unit.name or subdivisionData.name
|
||||
subdivisionData.preset = unit.preset or subdivisionData.preset
|
||||
subdivisionData.specDefault = unit.spec_def or subdivisionData.specDefault
|
||||
subdivisionData.model = unit.model or subdivisionData.model
|
||||
subdivisionData.skin = unit.skin or subdivisionData.skin
|
||||
subdivisionData.bodygroups = unit.bodygroups or subdivisionData.bodygroups
|
||||
end
|
||||
|
||||
if factionTable.Spec and factionTable.Spec[specIndex] then
|
||||
local spec = factionTable.Spec[specIndex]
|
||||
specData.name = spec.name or specData.name
|
||||
specData.weapons = spec.weapons or specData.weapons
|
||||
specData.podr = spec.podr or specData.podr
|
||||
end
|
||||
|
||||
if factionTable.Ranks and factionTable.Ranks[rankIndex] then
|
||||
local rank = factionTable.Ranks[rankIndex]
|
||||
rankData.name = rank[1] or rankData.name
|
||||
end
|
||||
end
|
||||
|
||||
local factionColor = nil
|
||||
if factionTable and factionTable.color then
|
||||
factionColor = { r = factionTable.color.r, g = factionTable.color.g, b = factionTable.color.b }
|
||||
end
|
||||
|
||||
local supplyStatus = "Стабильно"
|
||||
local minSupply = 0
|
||||
if arsenalPlugin and arsenalPlugin.config then
|
||||
minSupply = arsenalPlugin.config.minSupply or 0
|
||||
end
|
||||
|
||||
if supplyPoints <= minSupply then
|
||||
supplyStatus = "Критически низко"
|
||||
elseif supplyPoints <= minSupply * 2 and minSupply > 0 then
|
||||
supplyStatus = "Низко"
|
||||
end
|
||||
|
||||
return {
|
||||
timestamp = os.time(),
|
||||
faction = {
|
||||
id = factionID,
|
||||
name = factionTable and factionTable.name or "Неизвестно",
|
||||
description = factionTable and factionTable.description or "",
|
||||
color = factionColor,
|
||||
online = factionOnline,
|
||||
techPoints = techPoints,
|
||||
supplyPoints = supplyPoints,
|
||||
supplyStatus = supplyStatus,
|
||||
activeVehicles = activeVehicles
|
||||
},
|
||||
character = {
|
||||
name = char:GetName(),
|
||||
money = char:GetMoney(),
|
||||
id = char:GetID() or 0,
|
||||
rank = rankData,
|
||||
subdivision = subdivisionData,
|
||||
spec = specData
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
net.Receive("ix.F4_RequestInfo", function(_, client)
|
||||
if not IsValid(client) then return end
|
||||
local payload = BuildInfoPayload(client)
|
||||
if not payload then return end
|
||||
|
||||
net.Start("ix.F4_SendInfo")
|
||||
net.WriteTable(payload)
|
||||
net.Send(client)
|
||||
end)
|
||||
|
||||
local DONATE_PURCHASE_COOLDOWN = 2
|
||||
PLUGIN.donatePurchaseCooldowns = PLUGIN.donatePurchaseCooldowns or {}
|
||||
|
||||
local function GetCooldownKey(client)
|
||||
return client:SteamID64() or client:SteamID() or client:EntIndex()
|
||||
end
|
||||
|
||||
function PLUGIN:GetIGSBalance(client)
|
||||
if not IsValid(client) then return 0 end
|
||||
|
||||
if client.IGSFunds then
|
||||
local ok, funds = pcall(client.IGSFunds, client)
|
||||
if ok and isnumber(funds) then
|
||||
return math.max(math.floor(funds), 0)
|
||||
end
|
||||
end
|
||||
|
||||
if client.GetNetVar then
|
||||
local funds = client:GetNetVar("igsFunds", 0)
|
||||
if isnumber(funds) then
|
||||
return math.max(math.floor(funds), 0)
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function PLUGIN:AdjustIGSBalance(client, amount)
|
||||
if not IsValid(client) then return false, "Игрок недоступен" end
|
||||
amount = tonumber(amount) or 0
|
||||
if amount == 0 then return true end
|
||||
|
||||
if not client.AddIGSFunds then
|
||||
return false, "IGS недоступен"
|
||||
end
|
||||
|
||||
local ok, err = pcall(client.AddIGSFunds, client, amount)
|
||||
if ok then
|
||||
return true
|
||||
end
|
||||
|
||||
return false, err or "Ошибка IGS"
|
||||
end
|
||||
|
||||
function PLUGIN:IsDonateOnCooldown(client)
|
||||
local key = GetCooldownKey(client)
|
||||
local expires = self.donatePurchaseCooldowns[key] or 0
|
||||
if CurTime() < expires then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ArmDonateCooldown(client)
|
||||
local key = GetCooldownKey(client)
|
||||
self.donatePurchaseCooldowns[key] = CurTime() + DONATE_PURCHASE_COOLDOWN
|
||||
end
|
||||
|
||||
function PLUGIN:CanPurchaseDonateProduct(client, entry)
|
||||
local char = client:GetCharacter()
|
||||
if not char then
|
||||
return false, "Нет активного персонажа"
|
||||
end
|
||||
|
||||
if not entry then
|
||||
return false, "Предложение не найдено"
|
||||
end
|
||||
|
||||
if entry.oneTime then
|
||||
local history = char:GetData("donate_purchases", {})
|
||||
if history[entry.id] then
|
||||
return false, "Этот набор уже был приобретен"
|
||||
end
|
||||
end
|
||||
|
||||
local hookResult, hookMessage = hook.Run("CanPlayerBuyDonateProduct", client, entry)
|
||||
if hookResult == false then
|
||||
return false, hookMessage or "Покупка отклонена"
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function PLUGIN:GrantDonateItems(client, grantData)
|
||||
if not grantData then return end
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
|
||||
if istable(grantData.weapons) and #grantData.weapons > 0 then
|
||||
local owned = char:GetData("donate_weapons", {})
|
||||
local changed
|
||||
for _, class in ipairs(grantData.weapons) do
|
||||
if isstring(class) and not table.HasValue(owned, class) then
|
||||
table.insert(owned, class)
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
char:SetData("donate_weapons", owned)
|
||||
end
|
||||
end
|
||||
|
||||
if istable(grantData.cosmetics) and #grantData.cosmetics > 0 then
|
||||
local cosmetics = char:GetData("donate_cosmetics", {})
|
||||
for _, cosmeticID in ipairs(grantData.cosmetics) do
|
||||
if isstring(cosmeticID) then
|
||||
cosmetics[cosmeticID] = true
|
||||
end
|
||||
end
|
||||
char:SetData("donate_cosmetics", cosmetics)
|
||||
end
|
||||
|
||||
if istable(grantData.vehicles) and #grantData.vehicles > 0 then
|
||||
local vehicles = char:GetData("donate_vehicles", {})
|
||||
for _, certificate in ipairs(grantData.vehicles) do
|
||||
if isstring(certificate) then
|
||||
vehicles[#vehicles + 1] = certificate
|
||||
end
|
||||
end
|
||||
char:SetData("donate_vehicles", vehicles)
|
||||
end
|
||||
|
||||
if istable(grantData.boosts) and #grantData.boosts > 0 then
|
||||
local calls = char:GetData("donate_support_calls", {})
|
||||
for _, callID in ipairs(grantData.boosts) do
|
||||
calls[#calls + 1] = callID
|
||||
end
|
||||
char:SetData("donate_support_calls", calls)
|
||||
end
|
||||
|
||||
if istable(grantData.items) and #grantData.items > 0 then
|
||||
local pending = char:GetData("donate_items", {})
|
||||
for _, itemID in ipairs(grantData.items) do
|
||||
pending[#pending + 1] = { id = itemID, ts = os.time() }
|
||||
end
|
||||
char:SetData("donate_items", pending)
|
||||
end
|
||||
end
|
||||
|
||||
function PLUGIN:ApplyDonateReward(client, entry, mode)
|
||||
local char = client:GetCharacter()
|
||||
if not char then
|
||||
return false, "Нет активного персонажа"
|
||||
end
|
||||
|
||||
local reward = entry.reward
|
||||
if not reward then
|
||||
local hookResult, hookMessage = hook.Run("OnDonateReward", client, entry, mode)
|
||||
if hookResult ~= nil then
|
||||
return hookResult, hookMessage
|
||||
end
|
||||
return false, "Награда не настроена"
|
||||
end
|
||||
|
||||
local rewardType = reward.type
|
||||
|
||||
if rewardType == "privilege" then
|
||||
local tier = reward.tier or entry.id
|
||||
local duration = nil
|
||||
|
||||
if mode == "3month" then
|
||||
duration = os.time() + (90 * 86400) -- 90 дней
|
||||
else
|
||||
duration = os.time() + (30 * 86400) -- 30 дней (1 месяц)
|
||||
end
|
||||
|
||||
local privileges = char:GetData("donate_privileges", {})
|
||||
privileges[tier] = {
|
||||
tier = tier,
|
||||
expires = duration,
|
||||
granted = os.time(),
|
||||
mode = mode or "month"
|
||||
}
|
||||
char:SetData("donate_privileges", privileges)
|
||||
|
||||
-- Применяем группу через SAM если доступно
|
||||
if sam then
|
||||
local groupMap = {
|
||||
vip = "vip",
|
||||
vip_plus = "vip_plus",
|
||||
premium = "premium",
|
||||
sponsor = "sponsor"
|
||||
}
|
||||
local rank = groupMap[tier]
|
||||
if rank then
|
||||
local expireTime = mode == "3month" and os.time() + (90 * 86400) or os.time() + (30 * 86400)
|
||||
sam.player.set_rank(client, rank, expireTime)
|
||||
end
|
||||
end
|
||||
|
||||
local modeText = mode == "3month" and "на 3 месяца" or "на 1 месяц"
|
||||
return true, reward.successMessage or ("Привилегия " .. tier .. " активирована " .. modeText)
|
||||
|
||||
elseif rewardType == "weapon" then
|
||||
local weaponClass = reward.weaponClass or reward.class
|
||||
if not weaponClass then
|
||||
return false, "Класс оружия не указан"
|
||||
end
|
||||
|
||||
-- Добавляем оружие в список донат-оружия персонажа со сроком действия
|
||||
local donateWeapons = char:GetData("donate_weapons_timed", {})
|
||||
if not istable(donateWeapons) then donateWeapons = {} end
|
||||
|
||||
-- Вычисляем срок действия
|
||||
local expireTime
|
||||
if mode == "3month" then
|
||||
expireTime = os.time() + (90 * 86400) -- 90 дней
|
||||
else
|
||||
expireTime = os.time() + (30 * 86400) -- 30 дней
|
||||
end
|
||||
|
||||
-- Добавляем или обновляем срок
|
||||
donateWeapons[weaponClass] = {
|
||||
expires = expireTime,
|
||||
granted = os.time(),
|
||||
mode = mode or "1month"
|
||||
}
|
||||
char:SetData("donate_weapons_timed", donateWeapons)
|
||||
|
||||
local modeText = mode == "3month" and "на 3 месяца" or "на 1 месяц"
|
||||
return true, reward.successMessage or ("Донат-оружие " .. (reward.name or weaponClass) .. " добавлено в ваш арсенал " .. modeText)
|
||||
|
||||
elseif rewardType == "money" then
|
||||
local amount = reward.amount or 0
|
||||
if amount > 0 and char.GiveMoney then
|
||||
char:GiveMoney(math.floor(amount))
|
||||
return true, reward.successMessage or ("Вам начислено " .. amount .. " ₽")
|
||||
end
|
||||
return false, "Ошибка начисления денег"
|
||||
|
||||
elseif rewardType == "voice_chat" then
|
||||
-- Разблокировка голосового чата
|
||||
char:SetData("voice_chat_unlocked", true)
|
||||
|
||||
-- Если есть система SAM, можно добавить флаг
|
||||
if sam then
|
||||
-- Можно добавить специальную группу или флаг для голосового чата
|
||||
end
|
||||
|
||||
return true, reward.successMessage or "Голосовой чат разблокирован!"
|
||||
|
||||
elseif rewardType == "bundle" then
|
||||
if reward.money and reward.money > 0 and char.GiveMoney then
|
||||
char:GiveMoney(math.floor(reward.money))
|
||||
end
|
||||
|
||||
if reward.supply and reward.supply ~= 0 then
|
||||
local arsenal = ix.plugin.Get("arsenal")
|
||||
if arsenal and arsenal.AddFactionSupply then
|
||||
arsenal:AddFactionSupply(client:Team(), reward.supply)
|
||||
end
|
||||
end
|
||||
|
||||
if reward.techPoints and reward.techPoints ~= 0 then
|
||||
local vehicles = ix.plugin.Get("vehicles")
|
||||
if vehicles and vehicles.AddFactionPoints then
|
||||
vehicles:AddFactionPoints(client:Team(), reward.techPoints)
|
||||
end
|
||||
end
|
||||
|
||||
if reward.grantedItems then
|
||||
self:GrantDonateItems(client, reward.grantedItems)
|
||||
end
|
||||
|
||||
return true, reward.successMessage or "Комплект успешно выдан"
|
||||
|
||||
elseif rewardType == "pass" then
|
||||
local tier = reward.tier or entry.id
|
||||
local duration = math.max(1, tonumber(reward.durationDays) or 30) * 86400
|
||||
local passes = char:GetData("donate_passes", {})
|
||||
passes[tier] = {
|
||||
tier = tier,
|
||||
expires = os.time() + duration,
|
||||
bonuses = reward.bonuses or {}
|
||||
}
|
||||
char:SetData("donate_passes", passes)
|
||||
return true, reward.successMessage or "Подписка активирована"
|
||||
elseif rewardType == "booster" then
|
||||
local boosterID = reward.boosterID or entry.id
|
||||
local duration = math.max(1, tonumber(reward.durationHours) or 24) * 3600
|
||||
local boosters = char:GetData("donate_boosters", {})
|
||||
boosters[boosterID] = {
|
||||
expires = os.time() + duration,
|
||||
multiplier = reward.multiplier or 1.0
|
||||
}
|
||||
char:SetData("donate_boosters", boosters)
|
||||
return true, reward.successMessage or "Бустер активирован"
|
||||
elseif rewardType == "case" then
|
||||
local caseID = reward.caseID or entry.id
|
||||
local cases = char:GetData("donate_cases", {})
|
||||
cases[caseID] = (cases[caseID] or 0) + 1
|
||||
char:SetData("donate_cases", cases)
|
||||
return true, reward.successMessage or "Кейс добавлен в коллекцию"
|
||||
elseif rewardType == "cosmetic" then
|
||||
local unlockID = reward.unlockID or entry.id
|
||||
local cosmetics = char:GetData("donate_cosmetics", {})
|
||||
cosmetics[unlockID] = true
|
||||
char:SetData("donate_cosmetics", cosmetics)
|
||||
return true, reward.successMessage or "Косметика разблокирована"
|
||||
end
|
||||
|
||||
local hookResult, hookMessage = hook.Run("OnDonateReward", client, entry)
|
||||
if hookResult ~= nil then
|
||||
return hookResult, hookMessage
|
||||
end
|
||||
|
||||
return false, "Тип награды не поддерживается"
|
||||
end
|
||||
|
||||
function PLUGIN:MarkDonatePurchase(client, entry)
|
||||
local char = client:GetCharacter()
|
||||
if not char then return end
|
||||
|
||||
local history = char:GetData("donate_purchases", {})
|
||||
history[entry.id] = (history[entry.id] or 0) + 1
|
||||
char:SetData("donate_purchases", history)
|
||||
end
|
||||
|
||||
function PLUGIN:SendDonateResult(client, success, message, productID)
|
||||
if not IsValid(client) then return end
|
||||
|
||||
net.Start("ix.F4_DonatePurchaseResult")
|
||||
net.WriteBool(success and true or false)
|
||||
net.WriteString(message or "")
|
||||
net.WriteString(productID or "")
|
||||
net.Send(client)
|
||||
end
|
||||
|
||||
function PLUGIN:HandleDonatePurchase(client, productID, price, mode)
|
||||
local entry = self:GetDonateProduct(productID)
|
||||
local ok, msg = self:CanPurchaseDonateProduct(client, entry)
|
||||
if not ok then
|
||||
return false, msg, entry
|
||||
end
|
||||
|
||||
local actualPrice = price or tonumber(entry and entry.price) or 0
|
||||
if actualPrice <= 0 then
|
||||
return false, "Цена предложения не настроена", entry
|
||||
end
|
||||
|
||||
local funds = self:GetIGSBalance(client)
|
||||
if funds < actualPrice then
|
||||
return false, "Недостаточно средств на балансе", entry
|
||||
end
|
||||
|
||||
local debited, debitError = self:AdjustIGSBalance(client, -actualPrice)
|
||||
if not debited then
|
||||
return false, debitError or "Не удалось списать средства", entry
|
||||
end
|
||||
|
||||
local success, rewardMessage = self:ApplyDonateReward(client, entry, mode)
|
||||
if not success then
|
||||
self:AdjustIGSBalance(client, actualPrice)
|
||||
return false, rewardMessage or "Ошибка выдачи награды", entry
|
||||
end
|
||||
|
||||
self:MarkDonatePurchase(client, entry, mode)
|
||||
hook.Run("PostDonatePurchase", client, entry, actualPrice, mode)
|
||||
|
||||
return true, rewardMessage or "Покупка завершена", entry
|
||||
end
|
||||
|
||||
net.Receive("ix.F4_DonatePurchase", function(_, client)
|
||||
if not IsValid(client) or not client:IsPlayer() then return end
|
||||
|
||||
local productID = net.ReadString() or ""
|
||||
local price = net.ReadUInt(32) or 0
|
||||
local mode = net.ReadString() or "month"
|
||||
|
||||
if productID == "" then return end
|
||||
|
||||
if PLUGIN:IsDonateOnCooldown(client) then
|
||||
PLUGIN:SendDonateResult(client, false, "Слишком частые запросы", productID)
|
||||
return
|
||||
end
|
||||
|
||||
PLUGIN:ArmDonateCooldown(client)
|
||||
local success, message, entry = PLUGIN:HandleDonatePurchase(client, productID, price, mode)
|
||||
|
||||
local entryID = entry and entry.id or productID
|
||||
if success then
|
||||
local priceText = tostring(price)
|
||||
local modeText = mode == "forever" and " (навсегда)" or " (на месяц)"
|
||||
print(string.format("[F4 Donate] %s (%s) купил %s%s за %s", client:Name(), client:SteamID(), entryID, modeText, priceText))
|
||||
else
|
||||
print(string.format("[F4 Donate] Ошибка покупки %s игроком %s: %s", entryID, client:Name(), tostring(message)))
|
||||
end
|
||||
|
||||
PLUGIN:SendDonateResult(client, success, message, entryID)
|
||||
end)
|
||||
-- Admin command to add IGS funds
|
||||
ix.command.Add("GiveIGS", {
|
||||
description = "Выдать IGS средства игроку",
|
||||
CanRun = function(self, client)
|
||||
local userGroup = string.lower(client:GetUserGroup() or "user")
|
||||
return client:IsAdmin() or AdminPrivs[userGroup]
|
||||
end,
|
||||
arguments = {
|
||||
ix.type.player,
|
||||
ix.type.number
|
||||
},
|
||||
OnRun = function(self, client, target, amount)
|
||||
if not IsValid(target) then
|
||||
return "@invalidTarget"
|
||||
end
|
||||
|
||||
amount = math.floor(tonumber(amount) or 0)
|
||||
if amount == 0 then
|
||||
return "Укажите корректную сумму"
|
||||
end
|
||||
|
||||
local success, err = PLUGIN:AdjustIGSBalance(target, amount)
|
||||
if success then
|
||||
local action = amount > 0 and "выдал" or "снял"
|
||||
client:Notify(string.format("Вы %s %d IGS для %s", action, math.abs(amount), target:Name()))
|
||||
target:Notify(string.format("Администратор %s вам %d IGS", action == "выдал" and "выдал" or "снял у вас", math.abs(amount)))
|
||||
|
||||
print(string.format("[IGS Admin] %s (%s) %s %d IGS для %s (%s)",
|
||||
client:Name(), client:SteamID(), action, math.abs(amount), target:Name(), target:SteamID()))
|
||||
return ""
|
||||
else
|
||||
return err or "Ошибка при изменении баланса"
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
-- Admin command to check IGS balance
|
||||
ix.command.Add("CheckIGS", {
|
||||
description = "Проверить IGS баланс игрока",
|
||||
CanRun = function(self, client)
|
||||
local userGroup = string.lower(client:GetUserGroup() or "user")
|
||||
return client:IsAdmin() or AdminPrivs[userGroup]
|
||||
end,
|
||||
arguments = {
|
||||
ix.type.player
|
||||
},
|
||||
OnRun = function(self, client, target)
|
||||
if not IsValid(target) then
|
||||
return "@invalidTarget"
|
||||
end
|
||||
|
||||
local balance = PLUGIN:GetIGSBalance(target)
|
||||
return string.format("Баланс IGS игрока %s: %d руб.", target:Name(), balance)
|
||||
end
|
||||
})
|
||||
|
||||
net.Receive("ixChangeName", function(len, client)
|
||||
if not IsValid(client) then return end
|
||||
|
||||
local character = client:GetCharacter()
|
||||
if not character then return end
|
||||
|
||||
local newName = net.ReadString()
|
||||
if not newName or newName == "" then return end
|
||||
|
||||
if #newName < 3 or #newName > 50 then
|
||||
client:Notify("Имя должно содержать от 3 до 50 символов")
|
||||
return
|
||||
end
|
||||
|
||||
if newName:find("[<>\"\\/]") then
|
||||
client:Notify("Имя содержит недопустимые символы")
|
||||
return
|
||||
end
|
||||
|
||||
local oldName = character:GetName()
|
||||
character:SetName(newName)
|
||||
client:Notify("Ваше имя успешно изменено на: " .. newName)
|
||||
|
||||
net.Start("ixF4UpdateName")
|
||||
net.WriteString(newName)
|
||||
net.Send(client)
|
||||
|
||||
hook.Run("OnCharacterVarChanged", character, "name", oldName, newName)
|
||||
end)
|
||||
|
||||
-- Фикс выдачи донат-оружия IGS в Helix
|
||||
hook.Add("PostPlayerLoadout", "IGS_Helix_WeaponLoadoutFix", function(client)
|
||||
if not IsValid(client) or not IGS then return end
|
||||
|
||||
timer.Simple(1.5, function()
|
||||
if not IsValid(client) or not client:GetCharacter() then return end
|
||||
|
||||
if isfunction(IGS.PlayerLoadout) then
|
||||
IGS.PlayerLoadout(client)
|
||||
end
|
||||
|
||||
if isfunction(IGS.GetItems) and IsValid(client) and client.HasPurchase then
|
||||
for _, ITEM in pairs(IGS.GetItems()) do
|
||||
if not ITEM or not isfunction(ITEM.GetUID) then continue end
|
||||
|
||||
local weaponClass = (isfunction(ITEM.GetWeapon) and ITEM:GetWeapon()) or ITEM.weapon
|
||||
if weaponClass and client:HasPurchase(ITEM:GetUID()) then
|
||||
if not client:HasWeapon(weaponClass) then
|
||||
client:Give(weaponClass)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
@@ -0,0 +1,220 @@
|
||||
-- Клиентское меню строительства
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
if CLIENT then
|
||||
function DrawItems(items, panel, mode, main)
|
||||
for i=1, #items do
|
||||
if items[i].cat == mode then
|
||||
local DButton = panel:Add( "DButton" )
|
||||
DButton:SetHeight(60)
|
||||
DButton:SetFont( "Font1" )
|
||||
DButton:SetText( items[i].name .. " | Запас: " .. items[i].count )
|
||||
DButton:SetIcon( items[i].icon )
|
||||
DButton:Dock( TOP )
|
||||
DButton:DockMargin( 0, 10, 0, 10 )
|
||||
DButton.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(0, 0, 0, 255))
|
||||
draw.RoundedBox(5, 2, 2, w-4, h-4, Color(200, 200, 200, 255))
|
||||
end
|
||||
DButton.DoClick = function()
|
||||
if items[i].model != nil then
|
||||
net.Start("buildtrench")
|
||||
net.WriteInt(i, 13)
|
||||
net.SendToServer()
|
||||
main:Close()
|
||||
end
|
||||
if items[i].entity != nil then
|
||||
net.Start("setentity")
|
||||
net.WriteInt(i, 13)
|
||||
net.SendToServer()
|
||||
main:Close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
surface.CreateFont("Font3", {
|
||||
font = "Arial",
|
||||
extended = true,
|
||||
weight = 900,
|
||||
size = 24
|
||||
})
|
||||
surface.CreateFont("Font2", {
|
||||
font = "Arial",
|
||||
extended = true,
|
||||
weight = 800,
|
||||
size = 20
|
||||
})
|
||||
surface.CreateFont("Font", {
|
||||
font = "Arial",
|
||||
extended = true,
|
||||
size = 20
|
||||
})
|
||||
surface.CreateFont("Font1", {
|
||||
font = "Arial",
|
||||
weight = 900,
|
||||
extended = true,
|
||||
size = 20
|
||||
})
|
||||
local faded_black = Color(100, 100, 100, 0)
|
||||
|
||||
net.Receive("openmenu", function(len, ply)
|
||||
local alpha1 = 255
|
||||
local alpha2 = 0
|
||||
local alpha3 = 0
|
||||
local alpha4 = 0
|
||||
local catlabel
|
||||
local DScrollPanel
|
||||
local items = net.ReadTable()
|
||||
local mode = 0
|
||||
local main = vgui.Create("DFrame")
|
||||
main:SetSize(500, 500)
|
||||
main:Center()
|
||||
main:SetTitle(" ")
|
||||
main:SetDraggable(false)
|
||||
main:ShowCloseButton(false)
|
||||
main:MakePopup()
|
||||
main.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, faded_black)
|
||||
end
|
||||
|
||||
local p = vgui.Create( "DPanel", main )
|
||||
p:Dock( TOP )
|
||||
p:DockMargin( 0, 0, 0, 0 )
|
||||
p:SetHeight(40)
|
||||
p.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(0, 0, 0, 255))
|
||||
draw.RoundedBox(5, 2, 2, w-4, h-4, Color(150, 150, 150, 255))
|
||||
end
|
||||
local namelabel = vgui.Create( "DLabel", p )
|
||||
namelabel:Dock( LEFT )
|
||||
namelabel:DockMargin( 20, 0, 20, 0 )
|
||||
namelabel:SetSize(200, 20)
|
||||
namelabel:SetFont( "Font" )
|
||||
namelabel:SetText( "Инженерный комплект" )
|
||||
|
||||
local closebutton = vgui.Create( "DButton", p )
|
||||
closebutton:Dock( RIGHT )
|
||||
closebutton:DockMargin( 20, 0, 20, 0 )
|
||||
closebutton:SetText( " " )
|
||||
closebutton:SetImage("entities/closebutton.png")
|
||||
closebutton:SetSize( 40, 40 )
|
||||
closebutton.Paint = function(self, w, h)
|
||||
draw.RoundedBox(2, 0, 0, w, h, Color(150, 150, 150, 0))
|
||||
end
|
||||
closebutton.DoClick = function()
|
||||
main:Close()
|
||||
end
|
||||
|
||||
local ppp = vgui.Create( "DPanel", main )
|
||||
ppp:Dock( TOP )
|
||||
ppp:DockMargin( 0, 20, 0, 20 )
|
||||
ppp:SetHeight(60)
|
||||
ppp.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(0, 0, 0, 255))
|
||||
draw.RoundedBox(5, 2, 2, w-4, h-4, Color(150, 150, 150, 255))
|
||||
end
|
||||
|
||||
local entity = vgui.Create( "DButton", ppp )
|
||||
entity:Dock( LEFT )
|
||||
entity:DockMargin( 70, 10, 0, 10 )
|
||||
entity:SetText( " " )
|
||||
entity:SetImage("entities/entity.png")
|
||||
entity:SetHeight(40)
|
||||
entity:SetWidth(40)
|
||||
entity.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(120, 120, 120, alpha1))
|
||||
end
|
||||
entity.DoClick = function()
|
||||
DScrollPanel:GetCanvas():Clear()
|
||||
DrawItems(items, DScrollPanel, 0, main)
|
||||
catlabel:SetText( " Объекты" )
|
||||
alpha1 = 255
|
||||
alpha2 = 0
|
||||
alpha3 = 0
|
||||
alpha4 = 0
|
||||
end
|
||||
|
||||
local tier1 = vgui.Create( "DButton", ppp )
|
||||
tier1:Dock( LEFT )
|
||||
tier1:DockMargin( 70, 10, 0, 10 )
|
||||
tier1:SetText( " " )
|
||||
tier1:SetImage("entities/tier1.png")
|
||||
tier1:SetHeight(40)
|
||||
tier1:SetWidth(40)
|
||||
tier1.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(120, 120, 120, alpha2))
|
||||
end
|
||||
tier1.DoClick = function()
|
||||
DScrollPanel:GetCanvas():Clear()
|
||||
DrawItems(items, DScrollPanel, 1, main)
|
||||
catlabel:SetText( "Легкие укрепления" )
|
||||
alpha1 = 0
|
||||
alpha2 = 255
|
||||
alpha3 = 0
|
||||
alpha4 = 0
|
||||
end
|
||||
|
||||
local tier2 = vgui.Create( "DButton", ppp )
|
||||
tier2:Dock( LEFT )
|
||||
tier2:DockMargin( 70, 10, 0, 10 )
|
||||
tier2:SetText( " " )
|
||||
tier2:SetImage("entities/tier2.png")
|
||||
tier2:SetHeight(40)
|
||||
tier2:SetWidth(40)
|
||||
tier2.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(120, 120, 120, alpha3))
|
||||
end
|
||||
tier2.DoClick = function()
|
||||
DScrollPanel:GetCanvas():Clear()
|
||||
DrawItems(items, DScrollPanel, 2, main)
|
||||
catlabel:SetText( "Средние укрепления" )
|
||||
alpha1 = 0
|
||||
alpha2 = 0
|
||||
alpha3 = 255
|
||||
alpha4 = 0
|
||||
end
|
||||
|
||||
local tier3 = vgui.Create( "DButton", ppp )
|
||||
tier3:Dock( LEFT )
|
||||
tier3:DockMargin( 70, 10, 0, 10 )
|
||||
tier3:SetText( " " )
|
||||
tier3:SetImage("entities/tier3.png")
|
||||
tier3:SetHeight(40)
|
||||
tier3:SetWidth(40)
|
||||
tier3.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(120, 120, 120, alpha4))
|
||||
end
|
||||
tier3.DoClick = function()
|
||||
DScrollPanel:GetCanvas():Clear()
|
||||
DrawItems(items, DScrollPanel, 3, main)
|
||||
catlabel:SetText( "Тяжелые укрепления" )
|
||||
alpha1 = 0
|
||||
alpha2 = 0
|
||||
alpha3 = 0
|
||||
alpha4 = 255
|
||||
end
|
||||
|
||||
local pp = vgui.Create( "DPanel", main )
|
||||
pp:Dock( TOP )
|
||||
pp:DockMargin( 0, 0, 0, 20 )
|
||||
pp:SetHeight(300)
|
||||
pp.Paint = function(self, w, h)
|
||||
draw.RoundedBox(5, 0, 0, w, h, Color(0, 0, 0, 255))
|
||||
draw.RoundedBox(5, 2, 2, w-4, h-4, Color(150, 150, 150, 255))
|
||||
end
|
||||
|
||||
catlabel = vgui.Create( "DLabel", pp )
|
||||
catlabel:Dock( TOP )
|
||||
catlabel:DockMargin( 170, 20, 0, 10 )
|
||||
catlabel:SetSize(350, 20)
|
||||
catlabel:SetFont( "Font" )
|
||||
catlabel:SetText( " Объекты" )
|
||||
|
||||
DScrollPanel = vgui.Create( "DScrollPanel", pp )
|
||||
DScrollPanel:Dock( TOP )
|
||||
DScrollPanel:SetHeight(240)
|
||||
DScrollPanel:DockMargin( 6, 6, 6, 6 )
|
||||
DrawItems(items, DScrollPanel, 0, main)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Клиентская часть смены скинов
|
||||
local PLUGIN = PLUGIN
|
||||
|
||||
local function OpenSkinMenu(ply, ent)
|
||||
if not IsValid(ent) then return end
|
||||
|
||||
local frame = vgui.Create("DFrame")
|
||||
frame:SetSize(250, 400)
|
||||
frame:SetTitle("Выбор скина")
|
||||
frame:Center()
|
||||
frame:MakePopup()
|
||||
|
||||
local skinPanel = vgui.Create("DScrollPanel", frame)
|
||||
skinPanel:SetSize(230, 350)
|
||||
skinPanel:SetPos(10, 30)
|
||||
|
||||
net.Start("RequestSkinData")
|
||||
net.WriteEntity(ent)
|
||||
net.SendToServer()
|
||||
|
||||
net.Receive("ReceiveSkinData", function()
|
||||
local totalSkins = net.ReadInt(8)
|
||||
local skinNames = net.ReadTable()
|
||||
for i = 0, totalSkins - 1 do
|
||||
local skinName = skinNames[i] or "Скин " .. i
|
||||
local button = vgui.Create("DButton", skinPanel)
|
||||
button:SetText(skinName)
|
||||
button:SetSize(210, 40)
|
||||
button:SetPos(10, 10 + i * 50)
|
||||
button.DoClick = function()
|
||||
net.Start("ChangeEntitySkin")
|
||||
net.WriteEntity(ent)
|
||||
net.WriteInt(i, 8)
|
||||
net.SendToServer()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
hook.Add("PlayerBindPress", "OpenSkinMenuOnUse", function(ply, bind, pressed)
|
||||
local allowedGroups = {
|
||||
owner = true,
|
||||
admin = true,
|
||||
["spec admin"] = true,
|
||||
prem = true,
|
||||
sponsor = true
|
||||
}
|
||||
|
||||
if allowedGroups[ply:GetUserGroup()] then
|
||||
if bind == "+use" and pressed then
|
||||
local trace = ply:GetEyeTrace()
|
||||
local ent = trace.Entity
|
||||
|
||||
if IsValid(ent) and trace.HitPos:Distance(ply:GetPos()) < 1000 then
|
||||
local activeWeapon = ply:GetActiveWeapon()
|
||||
if IsValid(activeWeapon) and activeWeapon:GetClass() == "weapon_hands" then
|
||||
net.Start("CheckAllowedEntity")
|
||||
net.WriteEntity(ent)
|
||||
net.SendToServer()
|
||||
net.Receive("EntityAllowed", function()
|
||||
OpenSkinMenu(ply, ent)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
concommand.Add("get_bodygroup_info", function(ply)
|
||||
local trace = ply:GetEyeTrace()
|
||||
local ent = trace.Entity
|
||||
|
||||
if not IsValid(ent) then
|
||||
print("[Ошибка] Вы не смотрите на объект!")
|
||||
return
|
||||
end
|
||||
|
||||
local bodygroups = ent:GetBodyGroups()
|
||||
print("========[ Bodygroup Info ]========")
|
||||
print("Модель: " .. ent:GetModel())
|
||||
|
||||
for _, bg in ipairs(bodygroups) do
|
||||
local currentValue = ent:GetBodygroup(bg.id)
|
||||
print(string.format("Bodygroup: %s (ID: %d) | Выбрано: %d", bg.name, bg.id, currentValue))
|
||||
end
|
||||
|
||||
local materials = ent:GetMaterials()
|
||||
print("========[ Sub-Materials ]========")
|
||||
|
||||
for i = 0, #materials - 1 do
|
||||
local mat = ent:GetSubMaterial(i)
|
||||
if mat == "" then mat = "По умолчанию" end
|
||||
print(string.format("SubMaterial ID: %d | Материал: %s", i, mat))
|
||||
end
|
||||
end)
|
||||
|
||||
concommand.Add("get_skin_info", function(ply)
|
||||
local trace = ply:GetEyeTrace()
|
||||
local ent = trace.Entity
|
||||
|
||||
if not IsValid(ent) then
|
||||
print("[Ошибка] Вы не смотрите на объект!")
|
||||
return
|
||||
end
|
||||
|
||||
local totalSkins = ent:SkinCount() - 1
|
||||
print("========[ Skin Info ]========")
|
||||
print("Модель: " .. ent:GetModel())
|
||||
print("Доступно скинов: " .. totalSkins)
|
||||
|
||||
for i = 0, totalSkins do
|
||||
print(string.format("Skin ID: %d", i))
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,9 @@
|
||||
include("shared.lua")
|
||||
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
end
|
||||
|
||||
function ENT:IsTranslucent()
|
||||
return true
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
local StartPos
|
||||
local StartAng
|
||||
function ENT:Initialize()
|
||||
self:SetMoveType(MOVETYPE_NONE)
|
||||
StartPos = self:GetPos()
|
||||
StartAng = self:GetAngles()
|
||||
self:SetNWInt('MaxHp',self.hp)
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
self:SetNWInt('Hp',self.hp)
|
||||
if self:GetNWInt("Hp") > self:GetNWInt("MaxHp") then
|
||||
self:SetNWInt('Hp',self:GetNWInt("MaxHp"))
|
||||
end
|
||||
if SERVER then
|
||||
if self.poscoef == 0 and self.building then
|
||||
self.Owner:Freeze(false)
|
||||
self.Owner.building = false
|
||||
self:PhysicsInitStatic(6)
|
||||
self:SetCollisionGroup(0)
|
||||
self.building = false
|
||||
end
|
||||
if self.building then
|
||||
self.Owner.building = true
|
||||
self.poscoef = self.poscoef + 1
|
||||
self.Owner:Freeze(true)
|
||||
self:SetAngles(StartAng)
|
||||
self:SetPos(StartPos - self:GetAngles():Up() * -self.poscoef)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:OnRemove()
|
||||
if self.HasIdle then
|
||||
self:StopSound("TFA_INS2_RPG7.Loop")
|
||||
self.HasIdle = false
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage( dmg )
|
||||
if dmg:GetInflictor() == self or dmg:GetAttacker() == self then return end
|
||||
if self.Exploded then return end
|
||||
if self.hp > 0 and self.hp - dmg:GetDamage() <= 0 then
|
||||
self.Exploded = true
|
||||
self:Remove()
|
||||
end
|
||||
self.hp = self.hp - dmg:GetDamage()
|
||||
dmg:SetAttacker(self)
|
||||
dmg:SetInflictor(self)
|
||||
self:TakePhysicsDamage( dmg )
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.PrintName = "Contact Explosive"
|
||||
ENT.Author = ""
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
ENT.DoNotDuplicate = true
|
||||
ENT.DisableDuplicator = true
|
||||
@@ -0,0 +1,9 @@
|
||||
include("shared.lua")
|
||||
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
end
|
||||
|
||||
function ENT:IsTranslucent()
|
||||
return true
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
local StartPos
|
||||
local StartAng
|
||||
function ENT:Initialize()
|
||||
self:SetMoveType(MOVETYPE_NONE)
|
||||
StartPos = self:GetPos()
|
||||
StartAng = self:GetAngles()
|
||||
self:SetNWInt('MaxHp',self.hp)
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
self:SetNWInt('Hp',self.hp)
|
||||
if self:GetNWInt("Hp") > self:GetNWInt("MaxHp") then
|
||||
self:SetNWInt('Hp',self:GetNWInt("MaxHp"))
|
||||
end
|
||||
if SERVER then
|
||||
if self.poscoef == 0 and self.building then
|
||||
self.Owner:Freeze(false)
|
||||
self.Owner.building = false
|
||||
self:PhysicsInitStatic(6)
|
||||
self:SetCollisionGroup(0)
|
||||
self.building = false
|
||||
end
|
||||
if self.building then
|
||||
self.Owner.building = true
|
||||
self.poscoef = self.poscoef + 1
|
||||
self.Owner:Freeze(true)
|
||||
self:SetAngles(StartAng)
|
||||
self:SetPos(StartPos - self:GetAngles():Up() * -self.poscoef)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:OnRemove()
|
||||
if self.HasIdle then
|
||||
self:StopSound("TFA_INS2_RPG7.Loop")
|
||||
self.HasIdle = false
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage( dmg )
|
||||
if dmg:GetInflictor() == self or dmg:GetAttacker() == self then return end
|
||||
if self.Exploded then return end
|
||||
if self.hp > 0 and self.hp - dmg:GetDamage() <= 0 then
|
||||
self.Exploded = true
|
||||
self:Remove()
|
||||
end
|
||||
self.hp = self.hp - dmg:GetDamage()
|
||||
dmg:SetAttacker(self)
|
||||
dmg:SetInflictor(self)
|
||||
self:TakePhysicsDamage( dmg )
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.PrintName = "Contact Explosive"
|
||||
ENT.Author = ""
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
ENT.DoNotDuplicate = true
|
||||
ENT.DisableDuplicator = true
|
||||
@@ -0,0 +1,29 @@
|
||||
include("shared.lua")
|
||||
|
||||
surface.CreateFont( "PlayerTagFont", {
|
||||
font = "Arial",
|
||||
size = 72,
|
||||
} )
|
||||
|
||||
function ENT:Draw()
|
||||
self:DrawModel()
|
||||
angle = self:GetAngles() + Angle(0,90,90)
|
||||
pos = self:GetPos() + angle:Forward() * 0 + angle:Up() * 0 + angle:Right() * -30
|
||||
cam.Start3D2D( pos, angle, 0.05 )
|
||||
|
||||
surface.SetFont( "PlayerTagFont" )
|
||||
local tW, tH = surface.GetTextSize( self.DisplayName )
|
||||
|
||||
local padX = 20
|
||||
local padY = 5
|
||||
|
||||
surface.SetDrawColor( 0, 0, 0, 200 )
|
||||
surface.DrawRect( -tW / 2 - padX, -padY, tW + padX * 2, tH + padY * 2 )
|
||||
|
||||
draw.SimpleText( self.DisplayName, "PlayerTagFont", -tW / 2, 0, color_white )
|
||||
cam.End3D2D()
|
||||
end
|
||||
|
||||
function ENT:IsTranslucent()
|
||||
return true
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
AddCSLuaFile("cl_init.lua")
|
||||
AddCSLuaFile("shared.lua")
|
||||
include("shared.lua")
|
||||
ENT.hp = 500
|
||||
|
||||
function ENT:Initialize()
|
||||
self:SetModel(self.Model)
|
||||
self:PhysicsInit(6)
|
||||
|
||||
end
|
||||
|
||||
function ENT:Think()
|
||||
|
||||
end
|
||||
|
||||
function ENT:OnRemove()
|
||||
|
||||
end
|
||||
|
||||
function ENT:Use(activator, caller)
|
||||
if activator:GetActiveWeapon():GetClass() == "engineertool" or "engineertoolfpv" or "engineertoolmines" then
|
||||
print(activator:GetActiveWeapon().Structures)
|
||||
activator:GetActiveWeapon().Structures[self.ItemId].count = activator:GetActiveWeapon().Structures[self.ItemId].count + self.ItemCount
|
||||
net.Start('additem')
|
||||
net.WriteInt(self.ItemId, 13)
|
||||
net.WriteInt(self.ItemCount, 13)
|
||||
net.Send(activator)
|
||||
self:Remove()
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage( dmg )
|
||||
if dmg:GetInflictor() == self or dmg:GetAttacker() == self then return end
|
||||
if self.Exploded then return end
|
||||
if self.hp > 0 and self.hp - dmg:GetDamage() <= 0 then
|
||||
self.Exploded = true
|
||||
self:Remove()
|
||||
end
|
||||
self.hp = self.hp - dmg:GetDamage()
|
||||
dmg:SetAttacker(self)
|
||||
dmg:SetInflictor(self)
|
||||
self:TakePhysicsDamage( dmg )
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
ENT.Type = "anim"
|
||||
ENT.PrintName = "Ящик с припасами"
|
||||
ENT.Category = "Engineer Kit"
|
||||
ENT.Author = ""
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
ENT.Spawnable = true
|
||||
ENT.Model = "models/Items/ammocrate_smg1.mdl"
|
||||
ENT.ItemId = 1
|
||||
ENT.ItemCount = 1
|
||||
ENT.DisplayName = "ПТРК 'Корнет'"
|
||||
@@ -0,0 +1,251 @@
|
||||
if CLIENT then
|
||||
SWEP.WepSelectIcon = surface.GetTextureID("vgui/hud/weapon_doietoolus")
|
||||
SWEP.DrawWeaponInfoBox = false
|
||||
SWEP.BounceWeaponIcon = false
|
||||
killicon.Add( "weapon_doietoolus", "vgui/hud/weapon_doietoolus", Color( 0, 0, 0, 255 ) )
|
||||
end
|
||||
|
||||
SWEP.PrintName = "Entrenching Tool"
|
||||
|
||||
SWEP.Category = "Engiener Tools"
|
||||
|
||||
SWEP.Spawnable= true
|
||||
SWEP.AdminSpawnable= true
|
||||
SWEP.AdminOnly = false
|
||||
|
||||
SWEP.ViewModelFOV = 60
|
||||
SWEP.ViewModel = "models/weapons/doi/v_etool_us.mdl"
|
||||
SWEP.WorldModel = "models/weapons/doi/w_etool_us.mdl"
|
||||
SWEP.ViewModelFlip = false
|
||||
|
||||
SWEP.AutoSwitchTo = false
|
||||
SWEP.AutoSwitchFrom = false
|
||||
|
||||
SWEP.Slot = 0
|
||||
SWEP.SlotPos = 0
|
||||
|
||||
SWEP.UseHands = true
|
||||
|
||||
SWEP.HoldType = "melee2"
|
||||
|
||||
SWEP.FiresUnderwater = false
|
||||
|
||||
SWEP.DrawCrosshair = false
|
||||
|
||||
SWEP.DrawAmmo = true
|
||||
|
||||
SWEP.Base = "weapon_base"
|
||||
|
||||
SWEP.CSMuzzleFlashes = true
|
||||
|
||||
SWEP.Sprint = 0
|
||||
|
||||
SWEP.Primary.ClipSize = 0
|
||||
SWEP.Primary.DefaultClip = 0
|
||||
SWEP.Primary.Automatic = false
|
||||
SWEP.Primary.Ammo = "none"
|
||||
|
||||
SWEP.Secondary.ClipSize = 0
|
||||
SWEP.Secondary.DefaultClip = 0
|
||||
SWEP.Secondary.Automatic = false
|
||||
SWEP.Secondary.Ammo = "none"
|
||||
|
||||
SWEP.Structures = {
|
||||
{
|
||||
name = "Мешки с песком",
|
||||
icon = "entities/closebutton.png",
|
||||
cat = 1,
|
||||
hp = 10000,
|
||||
model = "models/props_fortifications/sandbags_corner1.mdl",
|
||||
count = 3,
|
||||
corrangle = Angle(0,0,0)
|
||||
},
|
||||
{
|
||||
name = "Большие мешки с песком",
|
||||
icon = "entities/closebutton.png",
|
||||
cat = 1,
|
||||
hp = 20000,
|
||||
model = "models/props_fortifications/sandbags_corner1_tall.mdl",
|
||||
count = 2,
|
||||
corrangle = Angle(0,0,0)
|
||||
},
|
||||
{
|
||||
name = "Противотанковые ёжи",
|
||||
icon = "entities/closebutton.png",
|
||||
cat = 1,
|
||||
hp = 50000,
|
||||
model = "models/test_sborka_props/barricades/ezi_antitank_2.mdl",
|
||||
count = 2,
|
||||
corrangle = Angle(0,-90,0)
|
||||
},
|
||||
{
|
||||
name = "Танковый окоп",
|
||||
icon = "entities/closebutton.png",
|
||||
cat = 2,
|
||||
hp = 100000,
|
||||
model = "models/trenches/prop/tank_cover/tank_cover.mdl",
|
||||
count = 3,
|
||||
corrangle = Angle(0,0,0)
|
||||
},
|
||||
}
|
||||
|
||||
function SWEP:Initialize()
|
||||
self:SetWeaponHoldType( self.HoldType )
|
||||
self:GetOwner().buildtime = 0
|
||||
end
|
||||
|
||||
function SWEP:Deploy()
|
||||
self:SetNextPrimaryFire( CurTime() + self.Owner:GetViewModel():SequenceDuration() )
|
||||
self.Weapon:SendWeaponAnim( ACT_VM_DRAW )
|
||||
Sprint = 0
|
||||
end
|
||||
|
||||
function SWEP:Holster()
|
||||
self.NextSecondaryAttack = 0
|
||||
Sprint = 0
|
||||
return true
|
||||
end
|
||||
|
||||
function SWEP:PrimaryAttack()
|
||||
if !(self.Weapon:GetNextPrimaryFire() < CurTime()) || not(Sprint == 0) then return end
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self.Owner:SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + 1)
|
||||
self:SetNextSecondaryFire( CurTime() + 1)
|
||||
|
||||
if SERVER then
|
||||
net.Start("openmenu")
|
||||
net.WriteTable(self.Structures)
|
||||
net.Send(self:GetOwner())
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function SWEP:SecondaryAttack()
|
||||
end
|
||||
|
||||
function SWEP:Reload()
|
||||
if SERVER then
|
||||
local item = self:GetOwner():GetEyeTrace().Entity
|
||||
if item:IsValid() then
|
||||
if item.id != nil then
|
||||
if item.Owner == self:GetOwner() then
|
||||
if (item:GetClass() == self.Structures[item.id].entity) or (item:GetModel() == self.Structures[item.id].model) then
|
||||
item:Remove()
|
||||
self.Structures[item.id].count = self.Structures[item.id].count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
function SWEP:DrawHUDBackground()
|
||||
if self:GetOwner().buildtime > 0 then
|
||||
draw.RoundedBox( 20, ScrW()/2-150, ScrH()/2-30, 0+300-self:GetOwner().buildtime*15, 60, Color( 50, 50, 50 , 180) )
|
||||
draw.RoundedBox( 20, ScrW()/2-145, ScrH()/2-25, 0+290-self:GetOwner().buildtime*14.5, 50, Color( 0, 255, 0 ,125) )
|
||||
draw.DrawText( tostring(100 - self:GetOwner().buildtime*5) .. '%', "Font3", ScrW()/2-20, ScrH()/2-15, Color( 255, 255, 255 ,255), 0 )
|
||||
end
|
||||
end
|
||||
|
||||
local delay = 0.5
|
||||
local nextOccurance = 0
|
||||
local flag1 = true
|
||||
if CLIENT then
|
||||
function SWEP:RenderScreen()
|
||||
if self.Owner.buildtime < 0 or Sprint == 2 then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
end
|
||||
|
||||
|
||||
if input.IsKeyDown(KEY_F) and flag1 then
|
||||
net.Start("buildstop12")
|
||||
net.SendToServer()
|
||||
self.Owner.building = false
|
||||
self.Owner.buildtime = -3
|
||||
flag1 = false
|
||||
timer.Simple(1, function() flag1 = true end)
|
||||
end
|
||||
|
||||
local timeLeft = nextOccurance - CurTime()
|
||||
if timeLeft < 0 and self:GetOwner().buildtime >= 0 then
|
||||
self:GetOwner().buildtime = self:GetOwner().buildtime - 0.5
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self:GetOwner():SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + delay)
|
||||
nextOccurance = CurTime() + delay
|
||||
end
|
||||
net.Receive("buildanim", function(len, ply)
|
||||
self:GetOwner().buildtime = 20
|
||||
end)
|
||||
end
|
||||
end
|
||||
if SERVER then
|
||||
function SWEP:Think()
|
||||
|
||||
|
||||
net.Receive("buildstop12", function(len, ply)
|
||||
ply:Freeze(false)
|
||||
if self.LastEnt:IsValid() then
|
||||
self.Structures[self.LastEnt.id].count = self.Structures[self.LastEnt.id].count + 1
|
||||
self.LastEnt:Remove()
|
||||
end
|
||||
end)
|
||||
net.Receive("buildtrench", function(len, ply)
|
||||
if ply:GetActiveWeapon():GetClass() != "engineertool" then return end
|
||||
local id = net.ReadInt(13)
|
||||
if (ply:GetEyeTrace().HitPos - ply:GetEyeTrace().StartPos):Length() <= 300 then
|
||||
if self.Structures[id].count > 0 then
|
||||
net.Start("buildanim")
|
||||
net.WriteEntity(item)
|
||||
net.Send(self:GetOwner())
|
||||
local item = ents.Create("util_structure")
|
||||
item:SetPos(ply:GetEyeTrace().HitPos)
|
||||
item:SetAngles(ply:GetEyeTrace().HitNormal:Angle() + Angle(90,ply:EyeAngles().y,0))
|
||||
item:SetModel(self.Structures[id].model)
|
||||
item.hp = self.Structures[id].hp
|
||||
item.id = id
|
||||
item.poscoef = -100
|
||||
item.building = true
|
||||
item.Owner = self:GetOwner()
|
||||
item:Spawn()
|
||||
self.LastEnt = item
|
||||
self.Structures[id].count = self.Structures[id].count - 1
|
||||
|
||||
end
|
||||
end
|
||||
end)
|
||||
net.Receive("setentity", function(len, ply)
|
||||
if ply:GetActiveWeapon():GetClass() != "engineertool" then return end
|
||||
local id = net.ReadInt(13)
|
||||
if (ply:GetEyeTrace().HitPos - ply:GetEyeTrace().StartPos):Length() <= 300 then
|
||||
if self.Structures[id].count > 0 then
|
||||
local item = ents.Create(self.Structures[id].entity)
|
||||
item:SetPos(ply:GetEyeTrace().HitPos + ply:GetEyeTrace().HitNormal * 40)
|
||||
item:SetAngles(ply:GetEyeTrace().HitNormal:Angle() + self.Structures[id].corrangle)
|
||||
item.id = id
|
||||
item.Owner = self:GetOwner()
|
||||
item:Spawn()
|
||||
self.LastEnt = item
|
||||
self.Structures[id].count = self.Structures[id].count - 1
|
||||
end
|
||||
end
|
||||
end)
|
||||
if (Sprint == 0) then
|
||||
|
||||
if self.Owner:KeyDown(IN_SPEED) and (self.Owner:KeyDown(IN_FORWARD) || self.Owner:KeyDown(IN_BACK) || self.Owner:KeyDown(IN_MOVELEFT) || self.Owner:KeyDown(IN_MOVERIGHT)) then
|
||||
Sprint = 1
|
||||
end
|
||||
end
|
||||
if (Sprint == 1) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_SPRINT_IDLE)
|
||||
Sprint = 2
|
||||
end
|
||||
if (Sprint == 2) then
|
||||
if self.Owner:KeyReleased(IN_SPEED) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
Sprint = 0
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,231 @@
|
||||
if CLIENT then
|
||||
|
||||
SWEP.WepSelectIcon = surface.GetTextureID("vgui/hud/weapon_doietoolus")
|
||||
SWEP.DrawWeaponInfoBox = false
|
||||
SWEP.BounceWeaponIcon = false
|
||||
killicon.Add( "weapon_doietoolus", "vgui/hud/weapon_doietoolus", Color( 0, 0, 0, 255 ) )
|
||||
end
|
||||
|
||||
SWEP.PrintName = "Entrenching Tool FPV"
|
||||
|
||||
SWEP.Category = "Engiener Tools"
|
||||
|
||||
SWEP.Spawnable= true
|
||||
SWEP.AdminSpawnable= true
|
||||
SWEP.AdminOnly = false
|
||||
|
||||
SWEP.ViewModelFOV = 60
|
||||
SWEP.ViewModel = "models/weapons/doi/v_etool_us.mdl"
|
||||
SWEP.WorldModel = "models/weapons/doi/w_etool_us.mdl"
|
||||
SWEP.ViewModelFlip = false
|
||||
|
||||
SWEP.AutoSwitchTo = false
|
||||
SWEP.AutoSwitchFrom = false
|
||||
|
||||
SWEP.Slot = 0
|
||||
SWEP.SlotPos = 0
|
||||
|
||||
SWEP.UseHands = true
|
||||
|
||||
SWEP.HoldType = "melee2"
|
||||
|
||||
SWEP.FiresUnderwater = false
|
||||
|
||||
SWEP.DrawCrosshair = false
|
||||
|
||||
SWEP.DrawAmmo = true
|
||||
|
||||
SWEP.Base = "weapon_base"
|
||||
|
||||
SWEP.CSMuzzleFlashes = true
|
||||
|
||||
SWEP.Sprint = 0
|
||||
|
||||
SWEP.Primary.ClipSize = 0
|
||||
SWEP.Primary.DefaultClip = 0
|
||||
SWEP.Primary.Automatic = false
|
||||
SWEP.Primary.Ammo = "none"
|
||||
|
||||
SWEP.Secondary.ClipSize = 0
|
||||
SWEP.Secondary.DefaultClip = 0
|
||||
SWEP.Secondary.Automatic = false
|
||||
SWEP.Secondary.Ammo = "none"
|
||||
|
||||
SWEP.Structures = {
|
||||
{
|
||||
name = "FPV-дрон",
|
||||
icon = "entities/entity.png",
|
||||
cat = 0,
|
||||
entity = "entity_drone_bomb",
|
||||
count = 2,
|
||||
id = 1,
|
||||
corrangle = Angle(90,0,0)
|
||||
},
|
||||
{
|
||||
name = "Разведывательный дрон",
|
||||
icon = "entities/entity.png",
|
||||
cat = 0,
|
||||
entity = "entity_drone_base",
|
||||
count = 1,
|
||||
id = 2,
|
||||
corrangle = Angle(90,0,0)
|
||||
},
|
||||
{
|
||||
name = "РЭБ",
|
||||
icon = "entities/entity.png",
|
||||
cat = 0,
|
||||
entity = "entity_dronejammer",
|
||||
count = 1,
|
||||
id = 3,
|
||||
corrangle = Angle(90,0,0)
|
||||
},
|
||||
{
|
||||
name = "Зарядная станция для дрона",
|
||||
icon = "entities/entity.png",
|
||||
cat = 0,
|
||||
entity = "entity_dronepad",
|
||||
count = 1,
|
||||
id = 4,
|
||||
corrangle = Angle(90,0,0)
|
||||
},
|
||||
}
|
||||
|
||||
function SWEP:Initialize()
|
||||
self:SetWeaponHoldType( self.HoldType )
|
||||
self:GetOwner().buildtime = 0
|
||||
end
|
||||
|
||||
function SWEP:Deploy()
|
||||
self:SetNextPrimaryFire( CurTime() + self.Owner:GetViewModel():SequenceDuration() )
|
||||
self.Weapon:SendWeaponAnim( ACT_VM_DRAW )
|
||||
Sprint = 0
|
||||
end
|
||||
|
||||
function SWEP:Holster()
|
||||
self.NextSecondaryAttack = 0
|
||||
Sprint = 0
|
||||
return true
|
||||
end
|
||||
|
||||
function SWEP:PrimaryAttack()
|
||||
if !(self.Weapon:GetNextPrimaryFire() < CurTime()) || not(Sprint == 0) then return end
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self.Owner:SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + 1)
|
||||
self:SetNextSecondaryFire( CurTime() + 1)
|
||||
|
||||
if SERVER then
|
||||
net.Start("openmenu")
|
||||
net.WriteTable(self.Structures)
|
||||
net.Send(self:GetOwner())
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function SWEP:SecondaryAttack()
|
||||
end
|
||||
|
||||
function SWEP:Reload()
|
||||
if SERVER then
|
||||
local item = self:GetOwner():GetEyeTrace().Entity
|
||||
if item:IsValid() then
|
||||
local structure_id = item.structure_id
|
||||
if structure_id != nil then
|
||||
if item:GetOwner() == self:GetOwner() then
|
||||
local struct = self.Structures[structure_id]
|
||||
if (item:GetClass() == struct.entity) or (item:GetModel() == struct.model) then
|
||||
item:Remove()
|
||||
struct.count = struct.count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
function SWEP:DrawHUDBackground()
|
||||
if self:GetOwner().buildtime > 0 then
|
||||
draw.RoundedBox( 20, ScrW()/2-150, ScrH()/2-30, 0+300-self:GetOwner().buildtime*15, 60, Color( 50, 50, 50 , 180) )
|
||||
draw.RoundedBox( 20, ScrW()/2-145, ScrH()/2-25, 0+290-self:GetOwner().buildtime*14.5, 50, Color( 0, 255, 0 ,125) )
|
||||
draw.DrawText( tostring(100 - self:GetOwner().buildtime*5) .. '%', "Font3", ScrW()/2-20, ScrH()/2-15, Color( 255, 255, 255 ,255), 0 )
|
||||
end
|
||||
end
|
||||
|
||||
local delay = 0.5
|
||||
local nextOccurance = 0
|
||||
local flag1 = true
|
||||
if CLIENT then
|
||||
function SWEP:RenderScreen()
|
||||
if self.Owner.buildtime < 0 or Sprint == 2 then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
end
|
||||
|
||||
|
||||
if input.IsKeyDown(KEY_F) and flag1 then
|
||||
net.Start("buildstop12")
|
||||
net.SendToServer()
|
||||
self.Owner.building = false
|
||||
self.Owner.buildtime = -3
|
||||
flag1 = false
|
||||
timer.Simple(1, function() flag1 = true end)
|
||||
end
|
||||
|
||||
local timeLeft = nextOccurance - CurTime()
|
||||
if timeLeft < 0 and self:GetOwner().buildtime >= 0 then
|
||||
self:GetOwner().buildtime = self:GetOwner().buildtime - 0.5
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self:GetOwner():SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + delay)
|
||||
nextOccurance = CurTime() + delay
|
||||
end
|
||||
net.Receive("buildanim", function(len, ply)
|
||||
self:GetOwner().buildtime = 20
|
||||
end)
|
||||
end
|
||||
end
|
||||
if SERVER then
|
||||
function SWEP:Think()
|
||||
net.Receive("buildstop12", function(len, ply)
|
||||
ply:Freeze(false)
|
||||
if self.LastEnt:IsValid() then
|
||||
self.Structures[self.LastEnt.id].count = self.Structures[self.LastEnt.id].count + 1
|
||||
self.LastEnt:Remove()
|
||||
end
|
||||
end)
|
||||
net.Receive("setentity", function(len, ply)
|
||||
if ply:GetActiveWeapon():GetClass() ~= "engineertoolfpv" then return end
|
||||
local id = net.ReadInt(13)
|
||||
local struct = self.Structures[id]
|
||||
|
||||
if struct and struct.count > 0 then
|
||||
local trace = ply:GetEyeTrace()
|
||||
if (trace.HitPos - trace.StartPos):Length() > 300 then return end
|
||||
|
||||
local item = ents.Create(struct.entity)
|
||||
item:SetPos(trace.HitPos + trace.HitNormal * 10)
|
||||
item:SetAngles(trace.HitNormal:Angle() + struct.corrangle)
|
||||
item:Spawn()
|
||||
struct.count = struct.count - 1
|
||||
item.structure_id = id
|
||||
if IsValid(ply) and ply:IsPlayer() then
|
||||
item:SetOwner(ply)
|
||||
end
|
||||
end
|
||||
end)
|
||||
if (Sprint == 0) then
|
||||
if self.Owner:KeyDown(IN_SPEED) and (self.Owner:KeyDown(IN_FORWARD) || self.Owner:KeyDown(IN_BACK) || self.Owner:KeyDown(IN_MOVELEFT) || self.Owner:KeyDown(IN_MOVERIGHT)) then
|
||||
Sprint = 1
|
||||
end
|
||||
end
|
||||
if (Sprint == 1) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_SPRINT_IDLE)
|
||||
Sprint = 2
|
||||
end
|
||||
if (Sprint == 2) then
|
||||
if self.Owner:KeyReleased(IN_SPEED) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
Sprint = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
if CLIENT then
|
||||
SWEP.WepSelectIcon = surface.GetTextureID("vgui/hud/weapon_doietoolus")
|
||||
SWEP.DrawWeaponInfoBox = false
|
||||
SWEP.BounceWeaponIcon = false
|
||||
killicon.Add( "weapon_doietoolus", "vgui/hud/weapon_doietoolus", Color( 0, 0, 0, 255 ) )
|
||||
end
|
||||
|
||||
SWEP.PrintName = "Entrenching Tool MINES"
|
||||
|
||||
SWEP.Category = "Engiener Tools"
|
||||
|
||||
SWEP.Spawnable= true
|
||||
SWEP.AdminSpawnable= true
|
||||
SWEP.AdminOnly = false
|
||||
|
||||
SWEP.ViewModelFOV = 60
|
||||
SWEP.ViewModel = "models/weapons/doi/v_etool_us.mdl"
|
||||
SWEP.WorldModel = "models/weapons/doi/w_etool_us.mdl"
|
||||
SWEP.ViewModelFlip = false
|
||||
|
||||
SWEP.AutoSwitchTo = false
|
||||
SWEP.AutoSwitchFrom = false
|
||||
|
||||
SWEP.Slot = 0
|
||||
SWEP.SlotPos = 0
|
||||
|
||||
SWEP.UseHands = true
|
||||
|
||||
SWEP.HoldType = "melee2"
|
||||
|
||||
SWEP.FiresUnderwater = false
|
||||
|
||||
SWEP.DrawCrosshair = false
|
||||
|
||||
SWEP.DrawAmmo = true
|
||||
|
||||
SWEP.Base = "weapon_base"
|
||||
|
||||
SWEP.CSMuzzleFlashes = true
|
||||
|
||||
SWEP.Sprint = 0
|
||||
|
||||
SWEP.Primary.ClipSize = 0
|
||||
SWEP.Primary.DefaultClip = 0
|
||||
SWEP.Primary.Automatic = false
|
||||
SWEP.Primary.Ammo = "none"
|
||||
|
||||
SWEP.Secondary.ClipSize = 0
|
||||
SWEP.Secondary.DefaultClip = 0
|
||||
SWEP.Secondary.Automatic = false
|
||||
SWEP.Secondary.Ammo = "none"
|
||||
|
||||
SWEP.Structures = {
|
||||
{
|
||||
name = "Мина 'ТМ-62'",
|
||||
icon = "entities/entity.png",
|
||||
cat = 0,
|
||||
entity = "sw_mine_tm62_v3",
|
||||
count = 10,
|
||||
corrangle = Angle(90,0,0)
|
||||
},
|
||||
}
|
||||
|
||||
function SWEP:Initialize()
|
||||
self:SetWeaponHoldType( self.HoldType )
|
||||
self:GetOwner().buildtime = 0
|
||||
end
|
||||
|
||||
function SWEP:Deploy()
|
||||
self:SetNextPrimaryFire( CurTime() + self.Owner:GetViewModel():SequenceDuration() )
|
||||
self.Weapon:SendWeaponAnim( ACT_VM_DRAW )
|
||||
Sprint = 0
|
||||
end
|
||||
|
||||
function SWEP:Holster()
|
||||
self.NextSecondaryAttack = 0
|
||||
Sprint = 0
|
||||
return true
|
||||
end
|
||||
|
||||
function SWEP:PrimaryAttack()
|
||||
if !(self.Weapon:GetNextPrimaryFire() < CurTime()) || not(Sprint == 0) then return end
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self.Owner:SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + 1)
|
||||
self:SetNextSecondaryFire( CurTime() + 1)
|
||||
|
||||
if SERVER then
|
||||
net.Start("openmenu")
|
||||
net.WriteTable(self.Structures)
|
||||
net.Send(self:GetOwner())
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function SWEP:SecondaryAttack()
|
||||
end
|
||||
|
||||
function SWEP:Reload()
|
||||
if SERVER then
|
||||
local item = self:GetOwner():GetEyeTrace().Entity
|
||||
if item:IsValid() then
|
||||
if item.id != nil then
|
||||
if item.Owner == self:GetOwner() then
|
||||
if (item:GetClass() == self.Structures[item.id].entity) or (item:GetModel() == self.Structures[item.id].model) then
|
||||
item:Remove()
|
||||
self.Structures[item.id].count = self.Structures[item.id].count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
function SWEP:DrawHUDBackground()
|
||||
if self:GetOwner().buildtime > 0 then
|
||||
draw.RoundedBox( 20, ScrW()/2-150, ScrH()/2-30, 0+300-self:GetOwner().buildtime*15, 60, Color( 50, 50, 50 , 180) )
|
||||
draw.RoundedBox( 20, ScrW()/2-145, ScrH()/2-25, 0+290-self:GetOwner().buildtime*14.5, 50, Color( 0, 255, 0 ,125) )
|
||||
draw.DrawText( tostring(100 - self:GetOwner().buildtime*5) .. '%', "Font3", ScrW()/2-20, ScrH()/2-15, Color( 255, 255, 255 ,255), 0 )
|
||||
end
|
||||
end
|
||||
|
||||
local delay = 0.5
|
||||
local nextOccurance = 0
|
||||
local flag1 = true
|
||||
if CLIENT then
|
||||
function SWEP:RenderScreen()
|
||||
if self.Owner.buildtime < 0 or Sprint == 2 then
|
||||
--print(1)
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
end
|
||||
|
||||
|
||||
if input.IsKeyDown(KEY_F) and flag1 then
|
||||
net.Start("buildstop12")
|
||||
net.SendToServer()
|
||||
self.Owner.building = false
|
||||
self.Owner.buildtime = -3
|
||||
flag1 = false
|
||||
timer.Simple(1, function() flag1 = true end)
|
||||
end
|
||||
|
||||
local timeLeft = nextOccurance - CurTime()
|
||||
if timeLeft < 0 and self:GetOwner().buildtime >= 0 then
|
||||
self:GetOwner().buildtime = self:GetOwner().buildtime - 0.5
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
|
||||
self:GetOwner():SetAnimation( PLAYER_ATTACK1 )
|
||||
self:SetNextPrimaryFire( CurTime() + delay)
|
||||
nextOccurance = CurTime() + delay
|
||||
end
|
||||
net.Receive("buildanim", function(len, ply)
|
||||
print(2)
|
||||
self:GetOwner().buildtime = 20
|
||||
end)
|
||||
end
|
||||
end
|
||||
if SERVER then
|
||||
function SWEP:Think()
|
||||
|
||||
|
||||
net.Receive("buildstop12", function(len, ply)
|
||||
ply:Freeze(false)
|
||||
if self.LastEnt:IsValid() then
|
||||
self.Structures[self.LastEnt.id].count = self.Structures[self.LastEnt.id].count + 1
|
||||
self.LastEnt:Remove()
|
||||
end
|
||||
end)
|
||||
net.Receive("buildtrench", function(len, ply)
|
||||
if ply:GetActiveWeapon():GetClass() != "engineertool" then return end
|
||||
local id = net.ReadInt(13)
|
||||
if (ply:GetEyeTrace().HitPos - ply:GetEyeTrace().StartPos):Length() <= 300 then
|
||||
if self.Structures[id].count > 0 then
|
||||
net.Start("buildanim")
|
||||
net.WriteEntity(item)
|
||||
net.Send(self:GetOwner())
|
||||
local item = ents.Create("util_structure")
|
||||
item:SetPos(ply:GetEyeTrace().HitPos)
|
||||
item:SetAngles(ply:GetEyeTrace().HitNormal:Angle() + Angle(90,ply:EyeAngles().y,0))
|
||||
item:SetModel(self.Structures[id].model)
|
||||
item.hp = self.Structures[id].hp
|
||||
item.id = id
|
||||
item.poscoef = -100
|
||||
item.building = true
|
||||
item.Owner = self:GetOwner()
|
||||
item:Spawn()
|
||||
self.LastEnt = item
|
||||
self.Structures[id].count = self.Structures[id].count - 1
|
||||
|
||||
end
|
||||
end
|
||||
end)
|
||||
net.Receive("setentity", function(len, ply)
|
||||
if ply:GetActiveWeapon():GetClass() != "engineertoolmines" then return end
|
||||
local id = net.ReadInt(13)
|
||||
if (ply:GetEyeTrace().HitPos - ply:GetEyeTrace().StartPos):Length() <= 300 then
|
||||
if self.Structures[id].count > 0 then
|
||||
local item = ents.Create(self.Structures[id].entity)
|
||||
item:SetPos(ply:GetEyeTrace().HitPos + ply:GetEyeTrace().HitNormal * 40)
|
||||
item:SetAngles(ply:GetEyeTrace().HitNormal:Angle() + self.Structures[id].corrangle)
|
||||
item.id = id
|
||||
item.Owner = self:GetOwner()
|
||||
item:Spawn()
|
||||
self.LastEnt = item
|
||||
self.Structures[id].count = self.Structures[id].count - 1
|
||||
end
|
||||
end
|
||||
end)
|
||||
if (Sprint == 0) then
|
||||
|
||||
if self.Owner:KeyDown(IN_SPEED) and (self.Owner:KeyDown(IN_FORWARD) || self.Owner:KeyDown(IN_BACK) || self.Owner:KeyDown(IN_MOVELEFT) || self.Owner:KeyDown(IN_MOVERIGHT)) then
|
||||
Sprint = 1
|
||||
end
|
||||
end
|
||||
if (Sprint == 1) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_SPRINT_IDLE)
|
||||
Sprint = 2
|
||||
end
|
||||
if (Sprint == 2) then
|
||||
if self.Owner:KeyReleased(IN_SPEED) then
|
||||
self.Weapon:SendWeaponAnim(ACT_VM_IDLE)
|
||||
Sprint = 0
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user