add sborka

This commit is contained in:
2026-03-31 10:27:04 +03:00
commit f5e5f56c84
2345 changed files with 382127 additions and 0 deletions

View 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