Files
2026-03-31 10:27:04 +03:00

752 lines
30 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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