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