>
Guide 2026-04-17

Système de réputation pour FiveM

OntelMonke

OntelMonke

Administrateur et développeur chez Agency Scripts

Pourquoi les systèmes de réputation transforment le jeu de rôle

La plupart des serveurs FiveM traitent la progression des personnages comme binaire : soit tu as un travail, soit tu n'en avez pas. Il n’y a aucun sentiment de croissance, de maîtrise ou de statut mérité. Un système de réputation et de compétences change fondamentalement cela en offrant aux joueurs une progression mesurable qui récompense le temps investi et les activités réalisées. Lorsqu'un mécanicien répare son centième véhicule et débloque un réglage avancé du moteur, cet exploit semble mérité. Lorsqu'un criminel bâtit suffisamment de réputation dans la rue pour accéder à un équipement de braquage de haut niveau, il ressent un sentiment tangible de progression qui fait avancer son gameplay. Dans ce guide, nous allons construire un cadre complet de réputation et de compétences à partir de zéro, couvrant les calculs d'XP, les arbres de compétences, les niveaux de réputation, les capacités à débloquer et un système de prestige qui maintient l'engagement des joueurs de fin de partie.

Schéma de base de données pour les compétences des joueurs

La base de tout système de progression est un schéma de base de données bien conçu. Tu as besoin de tableaux qui suivent l'XP de compétence individuelle, les scores de réputation par faction ou activité et les capacités débloquées. Le schéma doit être suffisamment normalisé pour être interrogeable, mais suffisamment dénormalisé pour éviter des jointures coûteuses à chaque contrôle de compétence. Nous utilisons un seul player_skills table avec une clé composite d'identification de citoyen et de nom de compétence, plus une clé distincte player_reputation table pour la position des factions. Cette séparation permet de distinguer les compétences (capacités personnelles) de la réputation (comment les PNJ et les factions tu perçoivent), ce qui permet des interactions de jeu plus nuancées.

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

Calcul XP et courbes de nivellement

Une courbe XP plate où chaque niveau nécessite la même quantité d'expérience ne semble pas gratifiante, car les premiers niveaux passent trop vite tandis que les niveaux ultérieurs semblent identiques. La meilleure approche utilise une courbe polynomiale où chaque niveau nécessite progressivement plus d'XP, mais l'augmentation n'est pas si forte que le niveau maximum devienne inaccessible. Une formule courante est requiredXP = baseXP * (level ^ exponent) où une base de 100 et un exposant de 1,5 créent une courbe lisse. Le niveau 1 nécessite 100 XP, le niveau 10 nécessite environ 3 162 XP et le niveau 50 nécessite environ 35 355 XP. Cela permet de maintenir une progression rapide tout en donnant l'impression que les niveaux supérieurs sont de véritables réalisations qui nécessitent un jeu dédié pour être atteints.

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

Gestion XP côté serveur

Toutes les modifications XP doivent être effectuées côté serveur pour empêcher les exploits. Le serveur valide chaque demande d'octroi XP, vérifie la limitation du débit pour éviter le spam, applique tous les multiplicateurs actifs et conserve le résultat dans la base de données. Une décision de conception critique est de savoir s'il faut économiser sur chaque modification XP ou script par lots. Économiser sur chaque petit gain d'XP martèle la base de données, en particulier pour les compétences passives comme la distance de conduite. La solution est un cache en mémoire qui est vidé périodiquement dans la base de données, combiné à des sauvegardes immédiates pour les événements importants tels que les montées de niveau. Cela tu donne une précision en temps réel pour le joueur tout en gardant la charge de la base de données gérable.

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

Arbres de compétences et capacités déblocables

Les arbres de compétences ajoutent une couche stratégique en plus du nivellement brut. Au lieu que tous les joueurs de niveau 20 en mécanique soient identiques, les arbres de compétences leur permettent de se spécialiser. Un mécanicien peut investir des points dans les performances du moteur, ouvrant ainsi la possibilité d'ajouter des kits turbo, tandis qu'un autre se spécialise dans la carrosserie, ayant accès à des peintures et des livrées exclusives. Chaque niveau accorde un point de compétence qui peut être dépensé dans un arbre ramifié. La structure arborescente utilise une simple relation parent-enfant dans laquelle le déverrouillage d'un nœud enfant nécessite à la fois le déverrouillage du parent et un niveau de compétence minimum. Cela empêche les joueurs de se précipiter vers les capacités les plus puissantes sans acquérir les compétences fondamentales.

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

Niveaux de réputation et statut de faction

La réputation suit la façon dont les organisations et les factions perçoivent le joueur, indépendamment des compétences brutes. Un joueur peut être un conducteur expert mais avoir une mauvaise réputation auprès de la faction policière en raison de ses activités criminelles. La réputation est organisée en niveaux allant d'hostile à neutre en passant par vénéré, chaque niveau débloquant différentes options de dialogue, accès aux missions et prix des fournisseurs. Gagner de la réputation auprès d'une faction peut la réduire auprès des factions opposées, créant ainsi des compromis significatifs. Par exemple, effectuer des livraisons de drogue augmente la réputation du cartel mais diminue la réputation de la police. Ce système encourage les joueurs à faire des choix qui définissent leur personnage plutôt que de maximiser chaque faction simultanément.

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

Le système Prestige

Une fois qu’un joueur atteint le niveau maximum dans une compétence, il a besoin d’une raison pour continuer à s’engager dans cette activité. Le système de prestige permet aux joueurs de réinitialiser une compétence au niveau 1 en échange de bonus permanents : un badge de prestige cosmétique affiché à côté de leur nom, un bonus d'XP de 5 % par niveau de prestige sur cette compétence et l'accès à des éléments à débloquer exclusifs au prestige comme des modifications de véhicule uniques ou des recettes d'artisanat rares. Le compteur de prestige est illimité, mais chaque prestige successif prend plus de temps car le boost d'XP rend les niveaux précédents triviaux tandis que la courbe rattrape toujours son retard aux niveaux supérieurs. Affichez le nombre de prestige sous forme de chiffres romains ou d'étoiles à côté du nom de la compétence dans l'interface utilisateur pour donner un indicateur visuel clair du dévouement d'un joueur.

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

Interface utilisateur côté client et suivi XP passif

Le client gère le suivi XP passif pour des activités telles que la distance parcourue et le sprint, en envoyant des mises à jour périodiques au serveur. Une erreur courante consiste à envoyer un événement à chaque mètre parcouru, ce qui inonde le réseau. Au lieu de cela, accumulez la distance dans une variable locale et envoyez une mise à jour par lots toutes les 30 secondes. Pour l'interface utilisateur, créez un panneau de compétences accessible via une commande ou un raccourci clavier qui affiche toutes les compétences avec leur niveau actuel, la barre de progression XP, le nombre de prestige et l'arbre de compétences. Utilisez NUI avec un framework moderne comme React ou Vue pour la visualisation de l'arborescence, affichant les nœuds verrouillés sous forme de nœuds grisés et déverrouillés avec un effet lumineux. La barre de progression devrait s'animer en douceur lorsque de l'XP est gagnée, donnant un retour visuel satisfaisant qui renforce la boucle de progression et maintient les joueurs motivés pour continuer à développer leur personnage.

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.