```lua
-- cl_character_creation.lua
-- cl_character_creation.lua
include("autorun/shared_utils.lua")

function OpenCharacterCreation()
    print("[DEBUG] Client: Opening character creation menu")
    local frame = vgui.Create("EditablePanel")
    frame:SetSize(ScrW(), ScrH())
    frame:MakePopup()
    frame.Paint = function(self, w, h)
        DrawBlur(self, 6)
        draw.RoundedBox(0, 0, 0, w, h, Color(0, 0, 0, 220))
    end

    local mainPanel = frame:Add("DPanel")
    mainPanel:SetSize(ScrW() * 0.6, ScrH() * 0.6)
    mainPanel:Center()
    mainPanel.Paint = function(self, w, h)
        draw.RoundedBox(16, 0, 0, w, h, Color(28, 33, 40, 250))
    end

    local title = mainPanel:Add("DLabel")
    title:SetFont("CharacterCreation.Title")
    title:SetText("Создание персонажа")
    title:SizeToContents()
    title:SetPos(mainPanel:GetWide() / 2 - title:GetWide() / 2, 20)
    title:SetColor(Color(240, 240, 240))

    local characterData = { name = "", description = "" }

    local infoPanel = mainPanel:Add("DPanel")
    infoPanel:Dock(FILL)
    infoPanel:DockMargin(40, 80, 40, 40)
    infoPanel.Paint = nil

    local nameLabel = infoPanel:Add("DLabel")
    nameLabel:SetText("Имя персонажа:")
    nameLabel:SetFont("CharacterCreation.Label")
    nameLabel:Dock(TOP)
    nameLabel:DockMargin(0, 0, 0, 10)

    local nameEntry = infoPanel:Add("DTextEntry")
    nameEntry:Dock(TOP)
    nameEntry:DockMargin(0, 0, 0, 20)
    nameEntry.OnChange = function(self)
        characterData.name = self:GetValue()
    end

    local descLabel = infoPanel:Add("DLabel")
    descLabel:SetText("Описание персонажа:")
    descLabel:SetFont("CharacterCreation.Label")
    descLabel:Dock(TOP)
    descLabel:DockMargin(0, 0, 0, 10)

    local descEntry = infoPanel:Add("DTextEntry")
    descEntry:Dock(FILL)
    descEntry:DockMargin(0, 0, 0, 20)
    descEntry:SetMultiline(true)
    descEntry.OnChange = function(self)
        characterData.description = self:GetValue()
    end

    local nextBtn = infoPanel:Add("DButton")
    nextBtn:Dock(BOTTOM)
    nextBtn:SetText("Далее")
    nextBtn:SetFont("Faction.ClassButton")
    nextBtn:SetColor(Color(240, 240, 240))
    nextBtn:SizeToContentsX(30)
    nextBtn.Paint = nil
    nextBtn.DoClick = function()
        if characterData.name == "" then
            surface.PlaySound("buttons/button10.wav")
            LocalPlayer():ChatPrint("Ошибка: Укажите имя персонажа!")
            return
        end
        print("[DEBUG] Client: Sending CreateCharacter with name: " .. characterData.name)
        net.Start("CreateCharacter")
        net.WriteTable(characterData)
        net.SendToServer()
        timer.Simple(1, function()
            if IsValid(frame) then
                frame:Remove()
            end
        end)
    end

    local closeBtn = frame:Add("DButton")
    closeBtn:SetText("✕")
    closeBtn:SetFont("Faction.Title")
    closeBtn:SetColor(Color(240, 240, 240))
    closeBtn:SetPos(ScrW() - 60, 20)
    closeBtn:SizeToContentsX(30)
    closeBtn.Paint = nil
    closeBtn.DoClick = function()
        frame:Remove()
    end
end

net.Receive("OpenCharacterCreation", function()
    print("[DEBUG] Client: Received OpenCharacterCreation")
    OpenCharacterCreation()
end)

-- cl_character_selection.lua
-- cl_character_selection.lua
include("shared_utils.lua")

function OpenCharacterSelection(characters)
    print("[DEBUG] Client: Opening character selection menu")
    local frame = vgui.Create("EditablePanel")
    frame:SetSize(ScrW(), ScrH())
    frame:MakePopup()
    frame.Paint = function(self, w, h)
        DrawBlur(self, 6)
        draw.RoundedBox(0, 0, 0, w, h, Color(0, 0, 0, 220))
    end

    local mainPanel = frame:Add("DPanel")
    mainPanel:SetSize(ScrW() * 0.6, ScrH() * 0.6)
    mainPanel:Center()
    mainPanel.Paint = function(self, w, h)
        draw.RoundedBox(16, 0, 0, w, h, Color(28, 33, 40, 250))
    end

    local title = mainPanel:Add("DLabel")
    title:SetFont("CharacterSelection.Title")
    title:SetText("Выбор персонажа")
    title:SizeToContents()
    title:SetPos(mainPanel:GetWide() / 2 - title:GetWide() / 2, 20)
    title:SetColor(Color(240, 240, 240))

    local scroll = mainPanel:Add("DScrollPanel")
    scroll:Dock(FILL)
    scroll:DockMargin(40, 80, 40, 40)
    scroll:GetVBar():SetWidth(0)

    for charID, charData in pairs(characters) do
        local charButton = scroll:Add("DButton")
        charButton:Dock(TOP)
        charButton:DockMargin(0, 0, 0, 10)
        charButton:SetTall(60)
        charButton:SetText("")
        charButton.lerpColor = Color(40, 45, 50)
        charButton.Paint = function(self, w, h)
            self.lerpColor = LerpColor(0.1, self.lerpColor, self:IsHovered() and Color(50, 55, 60) or Color(40, 45, 50))
            draw.RoundedBox(8, 0, 0, w, h, self.lerpColor)
            draw.SimpleText(charData.name, "CharacterSelection.Label", 20, h / 2, Color(240, 240, 240), TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER)
            local factionName = "No Faction"
            if charData.factionUID ~= "" and FACTION_LIST and FACTION_LIST[charData.factionUID] then
                factionName = FACTION_LIST[charData.factionUID]:GetLocalized("name")
            end
            draw.SimpleText(factionName, "CharacterSelection.Label", w - 20, h / 2, Color(180, 180, 180), TEXT_ALIGN_RIGHT, TEXT_ALIGN_CENTER)
        end
        charButton.DoClick = function()
            surface.PlaySound("ui/buttonclick.wav")
            print("[DEBUG] Client: Selecting character " .. charID)
            net.Start("SelectCharacter")
            net.WriteString(charID)
            net.SendToServer()
            frame:Remove()
        end
    end

    local createBtn = mainPanel:Add("DButton")
    createBtn:Dock(BOTTOM)
    createBtn:SetText("Создать нового персонажа")
    createBtn:SetFont("CharacterSelection.Button")
    createBtn:SetColor(Color(240, 240, 240))
    createBtn:SizeToContentsX(30)
    createBtn.Paint = nil
    createBtn.DoClick = function()
        surface.PlaySound("ui/buttonclick.wav")
        print("[DEBUG] Client: Requesting new character creation")
        net.Start("RequestCharacterCreation")
        net.SendToServer()
        frame:Remove()
    end

    local closeBtn = frame:Add("DButton")
    closeBtn:SetText("✕")
    closeBtn:SetFont("CharacterSelection.Title")
    closeBtn:SetColor(Color(240, 240, 240))
    closeBtn:SetPos(ScrW() - 60, 20)
    closeBtn:SizeToContentsX(30)
    closeBtn.Paint = nil
    closeBtn.DoClick = function()
        frame:Remove()
    end
end

net.Receive("OpenCharacterSelection", function()
    local status, characters = pcall(net.ReadTable)
    if status and type(characters) == "table" then
        print("[DEBUG] Client: Received OpenCharacterSelection with " .. table.Count(characters) .. " characters")
        OpenCharacterSelection(characters)
    else
        print("[ERROR] Failed to read character list from server.")
    end
end)

-- cl_faction_system.lua
-- cl_faction_system.lua
include("autorun/shared_utils.lua")

local playerProgress = {}

local lang = GetClientLanguage()

local function GetLang()
    return lang
end

local function ShowClassSelection(faction, frame)
    if not faction or not faction.classes then
        print("[DEBUG] Client: No classes for faction " .. (faction.uid or "nil"))
        return
    end
    print("[DEBUG] Client: Showing class selection for faction: " .. faction.uid)
    local classPanel = vgui.Create("DPanel", frame)
    classPanel:SetSize(ScrW() * 0.8, ScrH() * 0.8)
    classPanel:SetPos(ScrW(), ScrH() * 0.1)
    classPanel:MoveTo(ScrW() * 0.1, ScrH() * 0.1, 0.5, 0, -1)
    classPanel.alpha = 0
    classPanel:AlphaTo(255, 0.3)
    classPanel.Paint = function(self, w, h) draw.RoundedBox(16, 0, 0, w, h, Color(28, 33, 40, 250)) end

    local title = classPanel:Add("DLabel")
    title:SetFont("Faction.Title")
    title:SetText("Выбор класса для " .. (faction:GetLocalized("name") or "Unknown"))
    title:SizeToContents()
    title:SetPos(classPanel:GetWide() / 2 - title:GetWide() / 2, 30)
    title:SetColor(Color(240, 240, 240))

    local scroll = classPanel:Add("DScrollPanel")
    scroll:Dock(FILL)
    scroll:DockMargin(40, 80, 40, 40)
    scroll:GetVBar():SetWidth(0)

    local grid = scroll:Add("DIconLayout")
    grid:Dock(FILL)
    grid:SetSpaceX(20)
    grid:SetSpaceY(20)

    for classID, class in SortedPairsByMemberValue(faction.classes or {}, "requiredLevel", false) do
        local card = grid:Add("DButton")
        local cardWidth = (classPanel:GetWide() - 3 * 20 - 2 * 40) / 4
        card:SetSize(cardWidth, 350)
        card:SetText("")
        card.lerpColor = Color(40, 45, 50)

        local isLocked = false
        local lockReason = ""
        local progressFraction = 0

        if class.requiredLevel and class.requiredClass then
            local requiredClassProgress = playerProgress[faction.uid] and playerProgress[faction.uid][class.requiredClass]
            if not requiredClassProgress or requiredClassProgress.level < class.requiredLevel then
                isLocked = true
                local reqClassName = faction.classes[class.requiredClass] and (faction.classes[class.requiredClass].name[GetLang()] or faction.classes[class.requiredClass].name.en) or "Unknown"
                lockReason = "Требуется " .. class.requiredLevel .. " уровень в классе " .. reqClassName
                progressFraction = requiredClassProgress and (requiredClassProgress.level / class.requiredLevel) or 0
            end
        end

        card.Paint = function(self, w, h)
            if isLocked then
                draw.RoundedBox(12, 0, 0, w, h, Color(30, 30, 30, 200))
                surface.SetDrawColor(100, 0, 0)
                surface.DrawRect(0, 0, w, 8)
                surface.SetMaterial(Material("icon16/lock.png"))
                surface.SetDrawColor(255, 255, 255)
                surface.DrawTexturedRect(w / 2 - 8, 100, 16, 16)
                draw.SimpleText(lockReason, "Faction.CardDesc", w / 2, 130, Color(200, 0, 0), TEXT_ALIGN_CENTER)
                surface.SetDrawColor(80, 80, 80)
                surface.DrawRect(20, 300, w - 40, 10)
                surface.SetDrawColor(faction.color or Color(255, 255, 255))
                surface.DrawRect(20, 300, (w - 40) * progressFraction, 10)

                local requiredClassProgress = playerProgress[faction.uid] and playerProgress[faction.uid][class.requiredClass] or {
                    level = 1,
                    exp = 0,
                    maxExp = 100
                }
                draw.SimpleText(requiredClassProgress.level .. "/" .. class.requiredLevel, "Faction.CardDesc", w / 2, 315, Color(200, 200, 200), TEXT_ALIGN_CENTER)
            else
                self.lerpColor = LerpColor(0.1, self.lerpColor, self:IsHovered() and Color(50, 55, 60) or Color(40, 45, 50))
                draw.RoundedBox(12, 0, 0, w, h, self.lerpColor)
                surface.SetDrawColor(faction.color or Color(255, 255, 255))
                surface.DrawRect(0, 0, w, 8)
                if class.icon then
                    surface.SetMaterial(Material(class.icon))
                    surface.SetDrawColor(255, 255, 255)
                    surface.DrawTexturedRect(25, 20, 200, 200)
                end
            end
        end

        local titleLabel = card:Add("DLabel")
        titleLabel:SetFont("Faction.CardTitle")
        titleLabel:SetText(class.name[GetLang()] or class.name.en or "Unknown")
        titleLabel:SizeToContents()
        titleLabel:SetPos(20, 230)
        titleLabel:SetColor(isLocked and Color(150, 150, 150) or (faction.color or Color(255, 255, 255)))

        local descLabel = card:Add("DLabel")
        descLabel:SetFont("Faction.CardDesc")
        descLabel:SetText(class.description[GetLang()] or class.description.en or "")
        descLabel:SetWrap(true)
        descLabel:SetSize(210, 60)
        descLabel:SetPos(20, 260)
        descLabel:SetColor(isLocked and Color(100, 100, 100) or Color(180, 180, 180))

        local progress = playerProgress[faction.uid] and playerProgress[faction.uid][classID] or {
            level = 1,
            exp = 0,
            maxExp = 100
        }

        local progressLabel = card:Add("DLabel")
        progressLabel:SetFont("Faction.CardDesc")
        progressLabel:SetText("Уровень: " .. progress.level .. " | Опыт: " .. progress.exp .. "/" .. progress.maxExp)
        progressLabel:SizeToContents()
        progressLabel:SetPos(20, 320)
        progressLabel:SetColor(isLocked and Color(100, 100, 100) or Color(200, 200, 200))

        if not isLocked then
            card.DoClick = function()
                surface.PlaySound("ui/buttonclick.wav")
                net.Start("Faction.ClassSelect")
                net.WriteString(classID)
                net.SendToServer()
                frame:Remove()
            end
        else
            card:SetEnabled(false)
            card:SetTooltip(lockReason)
        end
    end

    local backBtn = classPanel:Add("DButton")
    backBtn:SetText("Назад")
    backBtn:SetFont("Faction.ClassButton")
    backBtn:SetColor(Color(240, 240, 240))
    backBtn:SetPos(40, classPanel:GetTall() - 60)
    backBtn:SizeToContentsX(30)
    backBtn.Paint = nil
    backBtn.DoClick = function()
        surface.PlaySound("ui/buttonclickrelease.wav")
        classPanel:Remove()
    end
end

function OpenFactionMenu()
    if not FACTION_LIST then
        print("[DEBUG] Client: FACTION_LIST is nil")
        LocalPlayer():ChatPrint("Ошибка: Список фракций не загружен!")
        return
    end
    print("[DEBUG] Client: Opening faction menu")
    local frame = vgui.Create("EditablePanel")
    frame:SetSize(ScrW(), ScrH())
    frame:MakePopup()
    frame.Paint = function(self, w, h)
        DrawBlur(self, 6)
        draw.RoundedBox(0, 0, 0, w, h, Color(0, 0, 0, 220))
    end

    local closeBtn = frame:Add("DButton")
    closeBtn:SetText("✕")
    closeBtn:SetFont("Faction.Title")
    closeBtn:SetColor(Color(240, 240, 240))
    closeBtn:SetPos(ScrW() - 60, 20)
    closeBtn:SizeToContentsX(30)
    closeBtn.Paint = nil
    closeBtn.DoClick = function() frame:Remove() end

    local mainPanel = frame:Add("DPanel")
    mainPanel:SetSize(ScrW() * 0.8, ScrH() * 0.8)
    mainPanel:SetPos(ScrW(), ScrH() * 0.1)
    mainPanel:MoveTo(ScrW() * 0.1, ScrH() * 0.1, 0.5, 0, -1)
    mainPanel.alpha = 0
    mainPanel:AlphaTo(255, 0.3)
    mainPanel.Paint = function(self, w, h) draw.RoundedBox(16, 0, 0, w, h, Color(28, 33, 40, 250)) end

    local title = mainPanel:Add("DLabel")
    title:SetFont("Faction.Title")
    title:SetText("Выбор фракции")
    title:SizeToContents()
    title:SetPos(mainPanel:GetWide() / 2 - title:GetWide() / 2, 30)
    title:SetColor(Color(240, 240, 240))

    local scroll = mainPanel:Add("DScrollPanel")
    scroll:Dock(FILL)
    scroll:DockMargin(40, 80, 40, 40)
    scroll:GetVBar():SetWidth(0)

    local grid = scroll:Add("DIconLayout")
    grid:Dock(FILL)
    grid:SetSpaceX(20)
    grid:SetSpaceY(20)

    for uid, faction in pairs(FACTION_LIST) do
        local card = grid:Add("DButton")
        local cardWidth = (mainPanel:GetWide() - 3 * 20 - 2 * 40) / 4
        card:SetSize(cardWidth, 350)
        card:SetText("")
        card:SetTooltip("")
        card.faction = faction
        card.lerpColor = Color(40, 45, 50)
        card.Paint = function(self, w, h)
            self.lerpColor = LerpColor(0.1, self.lerpColor, self:IsHovered() and Color(50, 55, 60) or Color(40, 45, 50))
            draw.RoundedBox(12, 0, 0, w, h, self.lerpColor)
            surface.SetDrawColor(faction.color or Color(255, 255, 255))
            surface.DrawRect(0, 0, w, 8)
            if faction.icon then
                surface.SetMaterial(Material(faction.icon))
                surface.SetDrawColor(255, 255, 255)
                surface.DrawTexturedRect(25, 20, 200, 200)
            end
        end

        local titleLabel = card:Add("DLabel")
        titleLabel:SetFont("Faction.CardTitle")
        titleLabel:SetText(faction:GetLocalized("name"))
        titleLabel:SizeToContents()
        titleLabel:SetPos(20, 230)
        titleLabel:SetColor(faction.color or Color(255, 255, 255))

        local descLabel = card:Add("DLabel")
        descLabel:SetFont("Faction.CardDesc")
        descLabel:SetText(faction:GetLocalized("description"))
        descLabel:SetWrap(true)
        descLabel:SetSize(210, 60)
        descLabel:SetPos(20, 260)
        descLabel:SetColor(Color(180, 180, 180))

        card.DoClick = function()
            surface.PlaySound("ui/buttonclick.wav")
            net.Start("Faction.Select")
            net.WriteString(uid)
            net.SendToServer()
        end
    end
end

net.Receive("Faction.Selected", function()
    local factionUID = net.ReadString()
    print("[DEBUG] Client: Received Faction.Selected with factionUID: " .. factionUID)
    if factionUID == "" then
        OpenFactionMenu()
    elseif FACTION_LIST[factionUID] and FACTION_LIST[factionUID].classes and table.Count(FACTION_LIST[factionUID].classes) > 0 then
        local frame = vgui.Create("EditablePanel")
        frame:SetSize(ScrW(), ScrH())
        frame:MakePopup()
        frame.Paint = function(self, w, h)
            DrawBlur(self, 6)
            draw.RoundedBox(0, 0, 0, w, h, Color(0, 0, 0, 220))
        end
        ShowClassSelection(FACTION_LIST[factionUID], frame)
    else
        print("[DEBUG] Client: No classes for faction " .. factionUID .. ", closing")
    end
end)

net.Receive("Faction.ErrorSound", function()
    surface.PlaySound("buttons/button10.wav")
end)

local levelUpEffect = {
    active = false,
    startTime = 0,
    level = 1
}

net.Receive("Faction.UpdateProgress", function()
    local classID = net.ReadString()
    local progress = {
        level = net.ReadUInt(16),
        exp = net.ReadUInt(16),
        maxExp = net.ReadUInt(16)
    }
    local leveledUp = net.ReadBool()

    local factionUID = LocalPlayer():GetNWString("FactionUID")

    if not playerProgress[factionUID] then playerProgress[factionUID] = {} end
    playerProgress[factionUID][classID] = progress

    if leveledUp then
        levelUpEffect.active = true
        levelUpEffect.startTime = CurTime()
        levelUpEffect.level = progress.level
        surface.PlaySound("ambient/levels/citadel/zapper_warmup1.wav")
    end
end)

hook.Add("HUDPaint", "Faction.LevelUpEffect", function()
    if not levelUpEffect.active then return end
    local frac = math.Clamp((CurTime() - levelUpEffect.startTime) / 1, 0, 1)
    if frac >= 1 then
        levelUpEffect.active = false
        return
    end
    local alpha = Lerp(1 - frac, 150, 0)
    surface.SetDrawColor(255, 255, 0, alpha)
    surface.DrawRect(0, 0, ScrW(), ScrH())
    local text = "Level " .. levelUpEffect.level .. "!"
    surface.SetFont("Faction.Title")
    local textW, textH = surface.GetTextSize(text)
    surface.SetTextColor(255, 255, 255, Lerp(1 - frac, 255, 0))
    surface.SetTextPos(ScrW() / 2 - textW / 2, ScrH() / 2 - textH / 2)
    surface.DrawText(text)
end)

-- sh_faction_core.lua
-- sh_faction_core.lua
FACTION_LIST = FACTION_LIST or {}

local Faction = {}
Faction.__index = Faction

function Faction:new(uid, data)
    local obj = setmetatable({}, self)
    obj.uid = uid
    for k, v in pairs(data) do
        obj[k] = v
    end
    return obj
end

function Faction:GetLocalized(key)
    local lang = GetConVar("gmod_language") and GetConVar("gmod_language"):GetString() or "en"
    if self[key] then
        return self[key][lang] or self[key].en or "Unknown"
    end
    return "Unknown"
end

function LoadFactions()
    local files, _ = file.Find("factions/*.lua", "LUA")
    local teamIDs = {}
    for _, file in ipairs(files) do
        if SERVER then AddCSLuaFile("factions/" .. file) end
        local data = include("factions/" .. file)
        if data then
            local uid = data.uid or file:match("(.+)%.lua")
            if not FACTION_LIST[uid] then
                if data.teamID and not teamIDs[data.teamID] then
                    teamIDs[data.teamID] = true
                end
                FACTION_LIST[uid] = Faction:new(uid, data)
                print("[DEBUG] Loaded faction: " .. uid)
            end
        end
    end
end

hook.Add("Initialize", "LoadFactions", LoadFactions)

-- shared_utils.lua
-- shared_utils.lua

if SERVER then return end -- клиентская часть

-- Создаем шрифты один раз
surface.CreateFont("Faction.Title", {
    font = "Montserrat SemiBold",
    size = 32,
    weight = 600
})

surface.CreateFont("Faction.CardTitle", {
    font = "Open Sans SemiBold",
    size = 24,
    weight = 600
})

surface.CreateFont("Faction.CardDesc", {
    font = "Open Sans",
    size = 16,
    weight = 400
})

surface.CreateFont("Faction.ClassButton", {
    font = "Open Sans SemiBold",
    size = 20,
    weight = 600
})

surface.CreateFont("CharacterCreation.Title", {
    font = "Montserrat SemiBold",
    size = 32,
    weight = 600
})

surface.CreateFont("CharacterCreation.Label", {
    font = "Open Sans",
    size = 20,
    weight = 400
})

surface.CreateFont("CharacterSelection.Title", {
    font = "Montserrat SemiBold",
    size = 32,
    weight = 600
})

surface.CreateFont("CharacterSelection.Label", {
    font = "Open Sans",
    size = 20,
    weight = 400
})

surface.CreateFont("CharacterSelection.Button", {
    font = "Open Sans SemiBold",
    size = 20,
    weight = 600
})

-- Универсальная функция для размытия фона UI
local blur = Material("pp/blurscreen")
function DrawBlur(panel, amount)
    local x, y = panel:LocalToScreen(0, 0)
    surface.SetMaterial(blur)
    surface.SetDrawColor(255, 255, 255)
    for i = 1, 3 do
        blur:SetFloat("$blur", (i / 3) * amount)
        blur:Recompute()
        render.UpdateScreenEffectTexture()
        surface.DrawTexturedRect(-x, -y, ScrW(), ScrH())
    end
end

-- Линейная интерполяция цвета
function LerpColor(frac, from, to)
    return Color(
        Lerp(frac, from.r, to.r),
        Lerp(frac, from.g, to.g),
        Lerp(frac, from.b, to.b),
        Lerp(frac, from.a, to.a)
    )
end

-- Универсальная функция для получения языка клиента
function GetClientLanguage()
    local lang = "en"
    local convarLang = GetConVar("gmod_language")
    if convarLang then
        lang = convarLang:GetString() or "en"
    end
    return lang
end

-- sv_faction_system.lua
-- sv_faction_system.lua
if not SERVER then return end

include("autorun/sh_faction_core.lua")

-- Сеть
util.AddNetworkString("OpenCharacterSelection")
util.AddNetworkString("OpenCharacterCreation")
util.AddNetworkString("RequestCharacterCreation")
util.AddNetworkString("CreateCharacter")
util.AddNetworkString("SelectCharacter")
util.AddNetworkString("Faction.Select")
util.AddNetworkString("Faction.ClassSelect")
util.AddNetworkString("Faction.ErrorSound")
util.AddNetworkString("Faction.UpdateProgress")
util.AddNetworkString("Faction.Selected")

local teamCache = {}

hook.Add("Initialize", "SetupFactionTeams", function()
    if not FACTION_LIST then
        print("[DEBUG] Server: FACTION_LIST is nil in SetupFactionTeams")
        return
    end
    for uid, faction in pairs(FACTION_LIST) do
        if not teamCache[faction.teamID] and faction.teamID then
            team.SetUp(faction.teamID, faction:GetLocalized("name"), faction.color)
            teamCache[faction.teamID] = true
        end
    end
end)

local function LoadPlayerProgress(ply, factionUID, classID)
    local key = "FactionProgress_" .. factionUID .. "_" .. classID
    local progress = ply:GetPData(key, nil)
    if progress then
        local stat, tbl = pcall(util.JSONToTable, progress)
        if stat and type(tbl) == "table" then
            return tbl
        end
    end
    return { level = 1, exp = 0, maxExp = 100 }
end

local function SavePlayerProgress(ply, factionUID, classID, progress)
    if not progress then return end
    local json = util.TableToJSON(progress)
    if not json then return end
    local key = "FactionProgress_" .. factionUID .. "_" .. classID
    ply:SetPData(key, json)
end

local function AwardExperience(ply, exp)
    if not IsValid(ply) or not ply:Alive() then return end
    local factionUID = ply:GetNWString("FactionUID")
    local classID = ply:GetNWString("ClassID")
    local faction = FACTION_LIST[factionUID]
    if not faction or not faction.classes[classID] then
        return
    end

    local progress = LoadPlayerProgress(ply, factionUID, classID)
    progress.exp = progress.exp + exp
    local leveledUp = false

    while progress.exp >= progress.maxExp do
        progress.level = progress.level + 1
        progress.exp = progress.exp - progress.maxExp
        progress.maxExp = math.floor(progress.maxExp * 1.5)
        ply:ChatPrint("Поздравляем! Вы достигли уровня " .. progress.level .. "!")
        leveledUp = true

        if progress.level == 5 then
            ply:Give("weapon_ar2")
            ply:ChatPrint("Вы получили AR2 за достижение 5 уровня!")
        elseif progress.level == 10 then
            ply:Give("weapon_rpg")
            ply:ChatPrint("Вы получили RPG за достижение 10 уровня!")
        end
    end

    SavePlayerProgress(ply, factionUID, classID, progress)

    net.Start("Faction.UpdateProgress")
    net.WriteString(classID)
    net.WriteUInt(progress.level, 16)
    net.WriteUInt(progress.exp, 16)
    net.WriteUInt(progress.maxExp, 16)
    net.WriteBool(leveledUp)
    net.Send(ply)
end

local function LoadCharacters(ply)
    local steamid = ply:SteamID64()
    local path = "characters/" .. steamid .. "/"
    local characters = {}

    if file.Exists(path, "DATA") then
        local files = file.Find(path .. "*.json", "DATA")
        for _, charFile in ipairs(files) do
            local charID = charFile:match("(.+)%.json")
            local data = file.Read(path .. charFile, "DATA")
            local ok, char = pcall(util.JSONToTable, data)
            if ok and char and char.name then
                characters[charID] = char
                print("[DEBUG] Server: Loaded character " .. charID .. " for " .. ply:Nick() .. ": " .. char.name)
            end
        end
    end

    return characters
end

local function SaveCharacter(ply, charID, data)
    if not charID or not data then return end
    local steamid = ply:SteamID64()
    local path = "characters/" .. steamid .. "/" .. charID .. ".json"
    file.CreateDir("characters/" .. steamid)
    local json = util.TableToJSON(data, true)
    if json then
        file.Write(path, json)
        print("[DEBUG] Server: Saved character " .. charID .. " for " .. ply:Nick() .. ": " .. data.name)
    end
end

hook.Add("PlayerInitialSpawn", "ShowCharacterSelection", function(ply)
    timer.Simple(1, function()
        if not IsValid(ply) then return end
        local characters = LoadCharacters(ply)
        print("[DEBUG] Server: Sending OpenCharacterSelection to " .. ply:Nick() .. " with " .. table.Count(characters) .. " characters")
        net.Start("OpenCharacterSelection")
        net.WriteTable(characters)
        net.Send(ply)
    end)
end)

net.Receive("RequestCharacterCreation", function(len, ply)
    if not IsValid(ply) then return end
    print("[DEBUG] Server: Received RequestCharacterCreation from " .. ply:Nick())
    net.Start("OpenCharacterCreation")
    net.Send(ply)
end)

net.Receive("CreateCharacter", function(len, ply)
    if not IsValid(ply) then return end
    local data = net.ReadTable()
    print("[DEBUG] Server: Received CreateCharacter from " .. ply:Nick() .. ": " .. util.TableToJSON(data))

    if not data.name or data.name == "" then
        ply:ChatPrint("Ошибка: Имя персонажа не указано!")
        net.Start("Faction.ErrorSound")
        net.Send(ply)
        return
    end

    local characters = LoadCharacters(ply)
    local charID = "char" .. (table.Count(characters) + 1)
    local character = {
        name = data.name,
        description = data.description or "",
        factionUID = "",
        classID = ""
    }
    SaveCharacter(ply, charID, character)

    ply:SetNWString("CharacterID", charID)
    ply:SetNWString("CharacterName", character.name)
    ply:SetNWString("CharacterDescription", character.description)
    ply:SetNWString("FactionUID", "")
    ply:SetNWString("ClassID", "")

    print("[DEBUG] Server: Character " .. charID .. " created for " .. ply:Nick() .. ", opening faction menu")
    timer.Simple(0.5, function()
        if IsValid(ply) then
            OpenFactionMenu(ply)
        end
    end)
end)

net.Receive("SelectCharacter", function(len, ply)
    if not IsValid(ply) then return end
    local charID = net.ReadString()
    print("[DEBUG] Server: Received SelectCharacter " .. charID .. " from " .. ply:Nick())
    local characters = LoadCharacters(ply)
    local char = characters[charID]

    if not char then
        ply:ChatPrint("Ошибка: Персонаж не найден!")
        net.Start("Faction.ErrorSound")
        net.Send(ply)
        return
    end

    ply:SetNWString("CharacterID", charID)
    ply:SetNWString("CharacterName", char.name)
    ply:SetNWString("CharacterDescription", char.description)
    ply:SetNWString("FactionUID", char.factionUID or "")
    ply:SetNWString("ClassID", char.classID or "")

    print("[DEBUG] Server: Character " .. charID .. " selected for " .. ply:Nick())

    if char.factionUID == "" then
        print("[DEBUG] Server: No faction assigned, opening faction menu")
        timer.Simple(0.5, function()
            if IsValid(ply) then
                OpenFactionMenu(ply)
            end
        end)
    else
        print("[DEBUG] Server: Faction " .. char.factionUID .. " assigned, spawning")
        ply:Spawn()
        if char.classID == "" and FACTION_LIST[char.factionUID] and FACTION_LIST[char.factionUID].classes then
            net.Start("Faction.Selected")
            net.WriteString(char.factionUID)
            net.Send(ply)
        end
    end
end)

function OpenFactionMenu(ply)
    if not IsValid(ply) then return end
    print("[DEBUG] Server: Sending Faction.Selected to " .. ply:Nick())
    net.Start("Faction.Selected")
    net.WriteString("")
    net.Send(ply)
end

-- Ограничение частоты для AwardExperience (раз в 1 секунду максимум)
local lastExpTime = {}

hook.Add("PlayerDeath", "Faction.PlayerDeathExperience", function(victim, inflictor, attacker)
    if not IsValid(attacker) or not attacker:IsPlayer() or attacker == victim then return end

    local now = CurTime()
    if lastExpTime[attacker] and now - lastExpTime[attacker] < 1 then return end
    lastExpTime[attacker] = now

    local attackerFaction = FACTION_LIST[attacker:GetNWString("FactionUID")]
    local victimFaction = FACTION_LIST[victim:GetNWString("FactionUID")]

    if attackerFaction and victimFaction and attackerFaction.teamID ~= victimFaction.teamID then
        AwardExperience(attacker, 50)
    end
end)

hook.Add("OnNPCKilled", "Faction.NPCKilledExperience", function(npc, attacker, inflictor)
    if IsValid(attacker) and attacker:IsPlayer() then
        local now = CurTime()
        if lastExpTime[attacker] and now - lastExpTime[attacker] < 1 then return end
        lastExpTime[attacker] = now

        AwardExperience(attacker, 20)
    end
end)

net.Receive("Faction.Select", function(len, ply)
    if not IsValid(ply) or not ply:Alive() then return end

    local factionUID = net.ReadString()
    local faction = FACTION_LIST[factionUID]

    if not faction then
        ply:ChatPrint("Ошибка: Фракция не найдена!")
        net.Start("Faction.ErrorSound")
        net.Send(ply)
        return
    end

    if faction.maxPlayers then
        local playerCount = #team.GetPlayers(faction.teamID)
        if playerCount >= faction.maxPlayers then
            ply:ChatPrint(faction:GetLocalized("maxPlayersNotify") or "Эта фракция переполнена!")
            net.Start("Faction.ErrorSound")
            net.Send(ply)
            return
        end
    end

    ply:SetTeam(faction.teamID)
    ply:SetNWString("FactionUID", factionUID)

    local lang = ply:GetInfo("gmod_language") == "ru" and "ru" or "en"
    ply:ChatPrint(faction:GetLocalized("notify", lang) or "Вы выбрали фракцию: " .. faction:GetLocalized("name", lang))

    local charID = ply:GetNWString("CharacterID")
    if charID ~= "" then
        local characters = LoadCharacters(ply)
        local char = characters[charID]
        if char then
            char.factionUID = factionUID
            SaveCharacter(ply, charID, char)
        end
    end

    print("[DEBUG] Server: Faction " .. factionUID .. " selected by " .. ply:Nick())
    hook.Run("OnFactionSelected", ply, factionUID)

    net.Start("Faction.Selected")
    net.WriteString(factionUID)
    net.Send(ply)
end)

net.Receive("Faction.ClassSelect", function(len, ply)
    if not IsValid(ply) or not ply:Alive() then return end

    local factionUID = ply:GetNWString("FactionUID")
    local classID = net.ReadString()
    local faction = FACTION_LIST[factionUID]

    if not faction then
        ply:ChatPrint("Ошибка: Фракция не найдена!")
        net.Start("Faction.ErrorSound")
        net.Send(ply)
        return
    end

    if not faction.classes or not faction.classes[classID] then
        ply:ChatPrint("Ошибка: Класс не найден!")
        net.Start("Faction.ErrorSound")
        net.Send(ply)
        return
    end

    local class = faction.classes[classID]
    ply:SetNWString("ClassID", classID)

    ply:StripWeapons()

    local models = class.models or faction.models or {}
    if #models > 0 then
        ply:SetModel(table.Random(models))
    end

    local weapons = class.weapons or faction.weapons or {}
    for _, weapon in ipairs(weapons) do
        ply:Give(weapon)
    end

    local progress = LoadPlayerProgress(ply, factionUID, classID)

    if class.OnSpawn then
        class.OnSpawn(class, ply, progress)
    elseif faction.OnSpawn then
        faction.OnSpawn(faction, ply, progress)
    end

    local lang = ply:GetInfo("gmod_language") == "ru" and "ru" or "en"
    ply:ChatPrint("Вы выбрали класс: " .. (class.name[lang] or class.name.en or "Unknown"))

    local charID = ply:GetNWString("CharacterID")
    if charID ~= "" then
        local characters = LoadCharacters(ply)
        local char = characters[charID]
        if char then
            char.classID = classID
            SaveCharacter(ply, charID, char)
        end
    end

    print("[DEBUG] Server: Class " .. classID .. " selected by " .. ply:Nick())
    hook.Run("OnClassChanged", ply, factionUID, classID)

    net.Start("Faction.UpdateProgress")
    net.WriteString(classID)
    net.WriteUInt(progress.level, 16)
    net.WriteUInt(progress.exp, 16)
    net.WriteUInt(progress.maxExp, 16)
    net.WriteBool(false)
    net.Send(ply)
end)
```
Improve the character creation and selection interface. Optimize code, remove debugging functions, improve code. Fix possible bugs and inconsistencies, bugs in net and synchronization.