Система репутации 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, обеспечивая приятную визуальную обратную связь, которая усиливает цикл прогрессии и мотивирует игроков продолжать развивать персонажа.