Назад к блогу
Guide11 мин чтения

Система репутации FiveM: Статистика, навыки и прогресс

Добавьте репутацию и прогресс навыков в FiveM. Формулы статов, разблокировки, полосы интерфейса и лучшие скрипты репутации для более длительного вовлечения игроков.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Почему системы репутации трансформируют ролевую игру

Большинство серверов FiveM рассматривают прогрессию персонажа как бинарную: либо у вас есть работа, либо нет. Нет ощущения роста, мастерства или заслуженного статуса. Система репутации и навыков меняет это кардинально, давая игрокам измеримый прогресс, который вознаграждает вложенное время и выполненные действия. Когда механик ремонтирует сотый автомобиль и разблокирует продвинутую настройку двигателя, это достижение ощущается заслуженным. Когда преступник набирает достаточно уличной репутации для доступа к высокоуровневому ограбительскому оборудованию, появляется ощутимое чувство прогресса, движущее их игровой процесс вперёд. В этом руководстве мы создадим полную систему репутации и навыков с нуля, охватывая расчёты XP, деревья навыков, уровни репутации, разблокируемые способности и систему престижа, которая удерживает игроков на поздних этапах игры.

Схема базы данных для навыков игроков

Основа любой системы прогрессии , хорошо спроектированная схема базы данных. Нужны таблицы для отслеживания опыта навыков, очков репутации по фракциям или активностям и разблокированных способностей. Схема должна быть достаточно нормализована для запросов, но достаточно денормализована, чтобы избежать дорогих соединений при каждой проверке навыка. Мы используем единую player_skills таблица с составным ключом из ID гражданина и названия навыка, плюс отдельная player_reputation таблица для репутации фракции. Такое разделение отделяет навыки (личные способности) от репутации (как NPC и фракции вас воспринимают), что позволяет более тонко настраивать игровые взаимодействия.

-- SQL schema
CREATE TABLE IF NOT EXISTS player_skills (
    citizenid VARCHAR(50) NOT NULL,
    skill_name VARCHAR(50) NOT NULL,
    xp INT DEFAULT 0,
    level INT DEFAULT 1,
    prestige INT DEFAULT 0,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (citizenid, skill_name)
);

CREATE TABLE IF NOT EXISTS player_reputation (
    citizenid VARCHAR(50) NOT NULL,
    faction VARCHAR(50) NOT NULL,
    reputation INT DEFAULT 0,
    tier VARCHAR(20) DEFAULT 'neutral',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (citizenid, faction)
);

Расчёт опыта и кривые уровней

Плоская кривая опыта, где для каждого уровня требуется одинаковое количество очков, кажется неэффективной, так как ранние уровни проходят слишком быстро, а поздние ощущаются одинаковыми. Лучший подход , использовать полиномиальную кривую, где каждый уровень требует всё больше опыта, но рост не настолько резкий, чтобы максимальный уровень стал недостижимым. Распространённая формула , requiredXP = baseXP * (level ^ exponent) где основание 100 и показатель степени 1.5 создают плавную кривую. Уровень 1 требует 100 XP, уровень 10 примерно 3,162 XP, а уровень 50 около 35,355 XP. Это обеспечивает быструю начальную прокачку, при этом более высокие уровни ощущаются как настоящие достижения, требующие целенаправленной игры для их достижения.

-- shared/skills_config.lua
SkillsConfig = {}

SkillsConfig.BaseXP = 100
SkillsConfig.Exponent = 1.5
SkillsConfig.MaxLevel = 50

function SkillsConfig.GetRequiredXP(level)
    return math.floor(SkillsConfig.BaseXP * (level ^ SkillsConfig.Exponent))
end

function SkillsConfig.GetTotalXPForLevel(targetLevel)
    local total = 0
    for i = 1, targetLevel - 1 do
        total = total + SkillsConfig.GetRequiredXP(i)
    end
    return total
end

-- Skill definitions with XP sources
SkillsConfig.Skills = {
    driving = {
        label = 'Driving',
        icon = 'fa-car',
        xpSources = {
            { action = 'distance_driven', xpPer = 1, unit = 'per 500m' },
            { action = 'race_won', xpPer = 150, unit = 'per win' },
            { action = 'no_crash_streak', xpPer = 50, unit = 'per 5min' },
        }
    },
    shooting = {
        label = 'Marksmanship',
        icon = 'fa-crosshairs',
        xpSources = {
            { action = 'headshot', xpPer = 25, unit = 'per kill' },
            { action = 'bodyshot', xpPer = 10, unit = 'per kill' },
            { action = 'range_practice', xpPer = 5, unit = 'per target' },
        }
    },
    mechanic = {
        label = 'Mechanic',
        icon = 'fa-wrench',
        xpSources = {
            { action = 'vehicle_repaired', xpPer = 30, unit = 'per repair' },
            { action = 'engine_swap', xpPer = 100, unit = 'per swap' },
            { action = 'custom_tune', xpPer = 75, unit = 'per tune' },
        }
    },
    cooking = {
        label = 'Cooking',
        icon = 'fa-utensils',
        xpSources = {
            { action = 'meal_cooked', xpPer = 20, unit = 'per meal' },
            { action = 'recipe_discovered', xpPer = 50, unit = 'per recipe' },
        }
    },
    stamina = {
        label = 'Stamina',
        icon = 'fa-running',
        xpSources = {
            { action = 'distance_sprinted', xpPer = 1, unit = 'per 200m' },
            { action = 'swimming', xpPer = 2, unit = 'per 100m' },
        }
    },
}

Управление XP на стороне сервера

Все изменения XP должны происходить на стороне сервера для предотвращения эксплойтов. Сервер проверяет каждый запрос на начисление XP, контролирует частоту для предотвращения спама, применяет активные множители и сохраняет результат в базе данных. Критически важно решить, сохранять ли данные при каждом изменении XP или пакетно. Сохранение при каждом небольшом изменении сильно нагружает базу, особенно для пассивных навыков, таких как пробег. Решение , кэш в памяти с периодической записью в базу, а также немедленное сохранение при значимых событиях, например, повышении уровня. Это обеспечивает точность в реальном времени для игрока и управляемую нагрузку на базу.

-- server/skills_manager.lua
local playerCache = {}
local SAVE_INTERVAL = 60000 -- flush to DB every 60 seconds

-- Load player skills from DB on join
AddEventHandler('playerJoining', function()
    local src = source
    local citizenid = GetCitizenId(src)
    if not citizenid then return end

    local skills = MySQL.query.await('SELECT * FROM player_skills WHERE citizenid = ?', {citizenid})
    playerCache[citizenid] = {}
    for _, row in ipairs(skills or {}) do
        playerCache[citizenid][row.skill_name] = {
            xp = row.xp,
            level = row.level,
            prestige = row.prestige,
            dirty = false
        }
    end
end)

function AddSkillXP(citizenid, skillName, amount)
    if not SkillsConfig.Skills[skillName] then return end
    if not playerCache[citizenid] then return end

    local skill = playerCache[citizenid][skillName]
    if not skill then
        skill = { xp = 0, level = 1, prestige = 0, dirty = false }
        playerCache[citizenid][skillName] = skill
    end

    -- Apply prestige multiplier (each prestige = +5% XP)
    local multiplier = 1.0 + (skill.prestige * 0.05)
    local finalXP = math.floor(amount * multiplier)

    skill.xp = skill.xp + finalXP
    skill.dirty = true

    -- Check for level up
    local required = SkillsConfig.GetRequiredXP(skill.level)
    while skill.xp >= required and skill.level < SkillsConfig.MaxLevel do
        skill.xp = skill.xp - required
        skill.level = skill.level + 1
        required = SkillsConfig.GetRequiredXP(skill.level)

        -- Notify player of level up
        local src = GetPlayerByCitizenId(citizenid)
        if src then
            TriggerClientEvent('skills:levelUp', src, skillName, skill.level)
        end

        -- Immediate save on level up
        SaveSkill(citizenid, skillName, skill)
    end

    return skill
end

exports('AddSkillXP', AddSkillXP)

Деревья навыков и разблокируемые способности

Деревья навыков добавляют стратегический слой поверх обычного повышения уровня. Вместо того чтобы все игроки с уровнем 20 в механике были одинаковы, деревья навыков позволяют специализироваться. Один механик может вкладывать очки в производительность двигателя, открывая возможность установки турбокомпрессоров, другой специализируется на кузовном ремонте, получая доступ к эксклюзивным покраскам и ливреям. За каждый уровень даётся одно очко навыка, которое можно потратить в разветвлённом дереве. Структура дерева использует простые отношения родитель-ребёнок, где для разблокировки дочернего узла требуется, чтобы родитель был разблокирован и был достигнут минимальный уровень навыка. Это предотвращает стремление игроков сразу получить самые мощные способности без базовой подготовки.

-- shared/skill_trees.lua
SkillTrees = {
    mechanic = {
        { id = 'basic_repair', label = 'Basic Repair', reqLevel = 1, parent = nil,
          effect = { type = 'speed', value = 1.0 }, desc = 'Standard repair speed' },
        { id = 'fast_repair', label = 'Quick Hands', reqLevel = 5, parent = 'basic_repair',
          effect = { type = 'speed', value = 1.3 }, desc = '30% faster repairs' },
        { id = 'engine_diag', label = 'Engine Diagnostics', reqLevel = 10, parent = 'basic_repair',
          effect = { type = 'unlock', value = 'engine_scan' }, desc = 'Scan engine health remotely' },
        { id = 'turbo_kit', label = 'Turbo Installation', reqLevel = 20, parent = 'engine_diag',
          effect = { type = 'unlock', value = 'install_turbo' }, desc = 'Install turbo kits on vehicles' },
        { id = 'master_tune', label = 'Master Tuner', reqLevel = 35, parent = 'turbo_kit',
          effect = { type = 'unlock', value = 'advanced_tune' }, desc = 'Access to advanced ECU tuning' },
        { id = 'body_expert', label = 'Body Expert', reqLevel = 10, parent = 'basic_repair',
          effect = { type = 'unlock', value = 'custom_paint' }, desc = 'Unlock exclusive paint options' },
        { id = 'livery_master', label = 'Livery Master', reqLevel = 25, parent = 'body_expert',
          effect = { type = 'unlock', value = 'custom_livery' }, desc = 'Create and apply custom liveries' },
    },
}

-- server: unlock a skill tree node
function UnlockNode(citizenid, skillName, nodeId)
    local tree = SkillTrees[skillName]
    if not tree then return false, 'No tree for skill' end

    local node = nil
    for _, n in ipairs(tree) do
        if n.id == nodeId then node = n break end
    end
    if not node then return false, 'Node not found' end

    local skill = GetPlayerSkill(citizenid, skillName)
    if not skill or skill.level < node.reqLevel then
        return false, 'Level too low'
    end

    if node.parent then
        local parentUnlocked = IsNodeUnlocked(citizenid, skillName, node.parent)
        if not parentUnlocked then return false, 'Parent node locked' end
    end

    MySQL.insert('INSERT INTO player_skill_nodes (citizenid, skill_name, node_id) VALUES (?, ?, ?)',
        {citizenid, skillName, nodeId})

    return true
end

Уровни репутации и положение во фракции

Репутация отражает, как организации и фракции воспринимают игрока, отдельно от чистого навыка. Игрок может быть отличным водителем, но иметь плохую репутацию в полиции из-за преступной деятельности. Репутация организована в уровни от враждебного через нейтральный до почитаемого, каждый уровень открывает разные варианты диалогов, доступ к миссиям и цены у продавцов. Повышение репутации в одной фракции может снижать её в противоположных, создавая значимые компромиссы. Например, выполнение доставок наркотиков повышает репутацию в картеле, но снижает в полиции. Эта система поощряет игроков делать выбор, формирующий их персонажа, а не максимизировать репутацию во всех фракциях одновременно.

-- shared/reputation_config.lua
ReputationConfig = {
    tiers = {
        { name = 'hostile',   minRep = -1000, color = '#ef4444' },
        { name = 'unfriendly', minRep = -500, color = '#f97316' },
        { name = 'neutral',   minRep = 0,     color = '#94a3b8' },
        { name = 'friendly',  minRep = 500,   color = '#22c55e' },
        { name = 'honored',   minRep = 1500,  color = '#3b82f6' },
        { name = 'revered',   minRep = 3000,  color = '#a855f7' },
    },
    factions = {
        police   = { label = 'LSPD', opposing = {'cartel', 'gang_ballas'} },
        ems      = { label = 'EMS',  opposing = {} },
        cartel   = { label = 'Madrazo Cartel', opposing = {'police'} },
        mechanic = { label = 'LS Customs', opposing = {} },
        gang_ballas = { label = 'Ballas', opposing = {'police', 'gang_families'} },
        gang_families = { label = 'Families', opposing = {'gang_ballas'} },
    },
    opposingPenalty = 0.5, -- lose 50% of gained rep from opposing factions
}

-- server: modify reputation with faction cascading
function ModifyReputation(citizenid, faction, amount)
    local config = ReputationConfig.factions[faction]
    if not config then return end

    -- Apply to primary faction
    AdjustRep(citizenid, faction, amount)

    -- Penalize opposing factions
    if amount > 0 then
        for _, opposing in ipairs(config.opposing) do
            local penalty = math.floor(amount * ReputationConfig.opposingPenalty)
            AdjustRep(citizenid, opposing, -penalty)
        end
    end
end

Система престижа

Когда игрок достигает максимального уровня в навыке, ему нужна причина продолжать заниматься этим видом деятельности. Система престижа предоставляет такую возможность, позволяя игрокам сбросить навык до уровня 1 в обмен на постоянные бонусы: косметический значок престижа рядом с именем, 5% бонус к опыту за каждый уровень престижа в этом навыке и доступ к эксклюзивным разблокируемым предметам престижа, таким как уникальные модификации транспорта или редкие рецепты крафта. Счётчик престижа неограничен, но каждый следующий престиж занимает больше времени, так как бонус к опыту упрощает ранние уровни, а кривая всё равно догоняет на высоких уровнях. Отображайте количество престижа римскими цифрами или звёздами рядом с названием навыка в интерфейсе, чтобы ясно показать преданность игрока.

-- server: prestige a maxed skill
function PrestigeSkill(citizenid, skillName)
    local skill = playerCache[citizenid] and playerCache[citizenid][skillName]
    if not skill then return false, 'Skill not found' end
    if skill.level < SkillsConfig.MaxLevel then return false, 'Not max level' end

    -- Reset level and XP, increment prestige
    skill.level = 1
    skill.xp = 0
    skill.prestige = skill.prestige + 1
    skill.dirty = true

    -- Save immediately
    SaveSkill(citizenid, skillName, skill)

    -- Grant prestige reward
    local src = GetPlayerByCitizenId(citizenid)
    if src then
        TriggerClientEvent('skills:prestige', src, skillName, skill.prestige)
        -- Unlock prestige-specific items
        if skill.prestige == 1 then
            exports.ox_inventory:AddItem(src, 'prestige_badge_'..skillName, 1)
        elseif skill.prestige == 5 then
            exports.ox_inventory:AddItem(src, 'gold_tool_'..skillName, 1)
        end
    end

    return true, skill.prestige
end

RegisterNetEvent('skills:requestPrestige', function(skillName)
    local src = source
    local citizenid = GetCitizenId(src)
    local success, result = PrestigeSkill(citizenid, skillName)
    if success then
        lib.notify(src, { title = 'Prestige!', description = ('Prestige %d achieved for %s'):format(result, skillName), type = 'success' })
    else
        lib.notify(src, { title = 'Cannot Prestige', description = result, type = 'error' })
    end
end)

Клиентский UI и пассивное отслеживание XP

Клиент обрабатывает пассивное отслеживание XP для таких действий, как пройденное расстояние и спринт, отправляя периодические обновления на сервер. Распространённая ошибка , отправлять событие на каждый пройденный метр, что перегружает сеть. Вместо этого накапливайте расстояние в локальной переменной и отправляйте пакетное обновление каждые 30 секунд. Для UI создайте панель навыков, доступную через команду или привязку клавиши, которая показывает все навыки с их текущим уровнем, прогресс-баром XP, количеством престижей и деревом навыков. Используйте NUI с современным фреймворком, таким как React или Vue, для визуализации дерева, показывая заблокированные узлы серыми, а разблокированные , с эффектом свечения. Прогресс-бар должен плавно анимироваться при получении XP, обеспечивая приятную визуальную обратную связь, которая усиливает цикл прогрессии и мотивирует игроков продолжать развивать персонажа.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.