>
Guia 2026-04-17

Criar um Sistema de Reputação e Skills para FiveM

OntelMonke

OntelMonke

Admin & Developer na Agency Scripts

Por que os sistemas de reputação transformam o roleplay

A maioria dos servidores FiveM trata a progressão do personagem como binária: ou você tem um emprego ou não. Não há sensação de crescimento, domínio ou status conquistado. Um sistema de reputação e habilidades muda isso fundamentalmente, dando aos jogadores uma progressão mensurável que recompensa o tempo investido e as atividades concluídas. Quando um mecânico repara seu centésimo veículo e desbloqueia o ajuste avançado do motor, essa conquista parece merecida. Quando um criminoso constrói reputação suficiente nas ruas para acessar equipamentos de assalto de alto nível, há uma sensação tangível de progressão impulsionando sua jogabilidade. Neste guia, construiremos uma estrutura completa de reputação e habilidades do zero, cobrindo cálculos de XP, árvores de habilidades, níveis de reputação, habilidades desbloqueáveis ​​e um sistema de prestígio que mantém os jogadores finais engajados.

Esquema de banco de dados para habilidades do jogador

A base de qualquer sistema de progressão é um esquema de banco de dados bem projetado. Você precisa de tabelas que rastreiem XP de habilidades individuais, pontuações de reputação por facção ou atividade e habilidades desbloqueadas. O esquema deve ser normalizado o suficiente para ser consultado, mas desnormalizado o suficiente para evitar junções caras em cada verificação de habilidade. Usamos uma única tabela player_skills com uma chave composta de ID de cidadão e nome de habilidade, além de uma tabela player_reputation separada para posição de facção. Essa separação mantém as habilidades (habilidades pessoais) distintas da reputação (como os NPCs e as facções percebem você), o que permite interações de jogo mais diferenciadas.

-- 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)
);

Cálculo XP e curvas de nivelamento

Uma curva de XP plana, onde cada nível requer a mesma quantidade de experiência, parece pouco recompensadora porque os níveis iniciais passam muito rápido, enquanto os níveis posteriores parecem idênticos. A melhor abordagem usa uma curva polinomial onde cada nível requer progressivamente mais XP, mas o aumento não é tão acentuado que o nível máximo se torne inacessível. Uma fórmula comum é requiredXP = baseXP * (level ^ exponent) onde uma base de 100 e um expoente de 1,5 criam uma curva suave. O nível 1 requer 100 XP, o nível 10 requer cerca de 3.162 XP e o nível 50 requer cerca de 35.355 XP. Isso mantém a progressão inicial rápida, ao mesmo tempo que faz com que níveis mais altos pareçam conquistas genuínas que exigem jogo dedicado para serem alcançadas.

-- 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' },
        }
    },
}

Gerenciamento XP do lado do servidor

Todas as modificações do XP devem acontecer no lado do servidor para evitar explorações. O servidor valida cada solicitação de concessão de XP, verifica a limitação de taxa para evitar spam, aplica quaisquer multiplicadores ativos e persiste o resultado no banco de dados. Uma decisão crítica de projeto é economizar em cada alteração XP ou gravação em lote. Economizar em cada pequeno ganho de XP prejudica o banco de dados, especialmente para habilidades passivas, como distância de carro. A solução é um cache na memória que é liberado periodicamente no banco de dados, combinado com salvamentos imediatos para eventos significativos, como subidas de nível. Isso fornece precisão em tempo real para o player, ao mesmo tempo que mantém a carga do banco de dados gerenciável.

-- 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)

Árvores de habilidades e habilidades desbloqueáveis

As árvores de habilidades adicionam uma camada estratégica além do nivelamento bruto. Em vez de todos os jogadores no nível 20 em mecânica serem idênticos, as árvores de habilidades permitem que eles se especializem. Um mecânico pode investir pontos no desempenho do motor, desbloqueando a capacidade de adicionar kits turbo, enquanto outro se especializa em carroceria, ganhando acesso a pinturas e pinturas exclusivas. Cada nível concede um ponto de habilidade que pode ser gasto em uma árvore ramificada. A estrutura em árvore usa um relacionamento pai-filho simples, onde o desbloqueio de um nó filho requer que o nó pai seja desbloqueado e um nível mínimo de habilidade. Isso evita que os jogadores avancem para as habilidades mais poderosas sem desenvolver competências fundamentais.

-- 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

Níveis de reputação e posição da facção

A reputação rastreia como as organizações e facções percebem o jogador, independentemente da habilidade bruta. Um jogador pode ser um motorista experiente, mas ter uma péssima reputação junto à facção policial por causa de suas atividades criminosas. A reputação é organizada em níveis que variam de hostil, neutro a reverenciado, com cada nível desbloqueando diferentes opções de diálogo, acesso a missões e preços de fornecedores. Ganhar reputação com uma facção pode reduzi-la com facções opostas, criando compensações significativas. Por exemplo, completar as entregas de drogas aumenta a reputação do cartel, mas diminui a reputação da polícia. Este sistema incentiva os jogadores a fazerem escolhas que definam seu personagem, em vez de maximizar todas as facções simultaneamente.

-- 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

O Sistema de Prestígio

Quando um jogador atinge o nível máximo em uma habilidade, ele precisa de um motivo para continuar engajado nessa atividade. O sistema de prestígio permite que os jogadores redefinam uma habilidade de volta ao nível 1 em troca de bônus permanentes: um distintivo de prestígio cosmético exibido ao lado de seu nome, um aumento de 5% de XP por nível de prestígio nessa habilidade e acesso a itens desbloqueáveis ​​​​exclusivos de prestígio, como modificações exclusivas de veículos ou receitas de artesanato raras. O contador de prestígio é ilimitado, mas cada prestígio sucessivo leva mais tempo porque o aumento de XP torna os níveis anteriores triviais, enquanto a curva ainda se aproxima dos níveis mais altos. Exiba a contagem de prestígio como algarismos romanos ou estrelas ao lado do nome da habilidade na IU para fornecer um indicador visual claro da dedicação de um jogador.

-- 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 do lado do cliente e rastreamento XP passivo

O cliente lida com o rastreamento XP passivo para atividades como distância percorrida e corrida, enviando atualizações periódicas ao servidor. Um erro comum é enviar um evento a cada metro percorrido, o que inunda a rede. Em vez disso, acumule a distância em uma variável local e envie uma atualização em lote a cada 30 segundos. Para a IU, crie um painel de habilidades acessível por meio de um comando ou tecla que mostre todas as habilidades com seu nível atual, barra de progresso de XP, contagem de prestígio e árvore de habilidades. Use NUI com uma estrutura moderna como React ou Vue para a visualização em árvore, mostrando os nós bloqueados em cinza e os nós desbloqueados com um efeito de brilho. A barra de progresso deve ser animada suavemente quando o XP é ganho, fornecendo um feedback visual satisfatório que reforça o ciclo de progressão e mantém os jogadores motivados para continuar desenvolvendo seu personagem.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.