Zurück zum Blog
Guide11 Min. Lesezeit

FiveM-Rufsystem: Werte, Fähigkeiten und Fortschritt

Füge FiveM Ruf und Fähigkeitsfortschritt hinzu. Formeln für Werte, Freischaltungen, Anzeigen und die besten Rufskripte, damit Spieler länger dranbleiben.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Warum Reputationssysteme das Roleplay verändern

Die meisten FiveM-Server behandeln den Charakterfortschritt als Entweder-oder: Du hast einen Job oder nicht. Es gibt kein Gefühl von Entwicklung, Meisterschaft oder verdientem Status. Ein Reputations- und Fähigkeitensystem ändert das grundlegend, indem es Spielern einen messbaren Fortschritt bietet, der investierte Zeit und abgeschlossene Aktivitäten belohnt. Wenn ein Mechaniker sein hundertstes Fahrzeug repariert und die fortgeschrittene Motorabstimmung freischaltet, fühlt sich dieser Erfolg verdient an. Wenn ein Krimineller genug Straßenreputation aufgebaut hat, um Zugang zu hochwertiger Heist-Ausrüstung zu erhalten, treibt ein greifbarer Fortschritt das Gameplay voran. In diesem Guide erstellen wir von Grund auf ein vollständiges Reputations- und Fähigkeiten-Framework. Dabei behandeln wir XP-Berechnungen, Skill-Trees, Reputationsstufen, freischaltbare Fähigkeiten und ein Prestigesystem, das auch Endgame-Spieler bei der Stange hält.

Datenbankschema für Spielerfähigkeiten

Die Grundlage jedes Fortschrittssystems ist ein gut durchdachtes Datenbankschema. Du brauchst Tabellen, in denen die individuellen Skill-XP, Reputationswerte pro Fraktion oder Aktivität und freigeschaltete Fähigkeiten erfasst werden. Das Schema sollte ausreichend normalisiert sein, damit sich Daten abfragen lassen, aber zugleich genug denormalisiert, um kostspielige Joins bei jeder Fähigkeitsprüfung zu vermeiden. Wir verwenden eine einzelne player_skills Tabelle mit einem zusammengesetzten Schlüssel aus Citizen-ID und Skillname sowie eine separate player_reputation Tabelle für den Fraktionsstatus. Durch diese Trennung bleiben Fähigkeiten (persönliches Können) und Reputation (wie NPCs und Fraktionen dich wahrnehmen) voneinander abgegrenzt, was nuanciertere Gameplay-Interaktionen ermöglicht.

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

XP-Berechnung und Levelkurven

Eine flache XP-Kurve, bei der jedes Level gleich viel Erfahrung erfordert, fühlt sich wenig lohnend an, weil die ersten Level zu schnell erreicht werden und sich spätere Level gleich anfühlen. Am besten eignet sich eine polynomische Kurve, bei der jedes Level zunehmend mehr XP erfordert, der Anstieg aber nicht so steil ist, dass das Höchstlevel unerreichbar wird. Eine gängige Formel lautet requiredXP = baseXP * (level ^ exponent) wobei eine Basis von 100 und ein Exponent von 1.5 für eine gleichmäßige Kurve sorgen. Level 1 erfordert 100 XP, Level 10 etwa 3,162 XP und Level 50 ungefähr 35,355 XP. So schreitet der Fortschritt zu Beginn schnell voran, während höhere Level zu echten Erfolgen werden, für die engagiertes Spielen nötig ist.

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

Serverseitige XP-Verwaltung

Alle XP-Anpassungen müssen serverseitig erfolgen, um Exploits zu verhindern. Der Server validiert jede XP-Anfrage, prüft auf Rate Limits, um Spam zu verhindern, wendet aktive Multiplikatoren an und speichert das Ergebnis in der Datenbank. Eine wichtige Designentscheidung ist, ob bei jeder XP-Änderung gespeichert oder ob das Schreiben gebündelt wird. Bei jedem noch so kleinen XP-Zuwachs zu speichern, belastet die Datenbank stark, insbesondere bei passiven Fähigkeiten wie der gefahrenen Strecke. Die Lösung ist ein In-Memory-Cache, der in regelmäßigen Abständen in die Datenbank geschrieben wird, kombiniert mit sofortigen Speicherungen bei wichtigen Ereignissen wie Levelaufstiegen. So erhältst du für den Spieler Echtzeitgenauigkeit und hältst zugleich die Datenbanklast im Rahmen.

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

Skill-Trees und freischaltbare Fähigkeiten

Skill-Trees ergänzen das reine Leveln um eine strategische Ebene. Statt dass alle Mechaniker auf Level 20 identisch sind, können sie sich über Skill-Trees spezialisieren. Ein Mechaniker investiert vielleicht Punkte in die Motorleistung und schaltet den Einbau von Turbokits frei, während sich ein anderer auf Karosseriearbeiten spezialisiert und Zugriff auf exklusive Lackierungen und Liveries erhält. Mit jedem Level gibt es einen Skill-Punkt, der in einem verzweigten Baum ausgegeben werden kann. Die Baumstruktur basiert auf einer einfachen Eltern-Kind-Beziehung. Um einen untergeordneten Knoten freizuschalten, muss der übergeordnete Knoten freigeschaltet und ein Mindest-Skilllevel erreicht sein. So verhinderst du, dass Spieler ohne ausreichende Grundlagen direkt zu den mächtigsten Fähigkeiten gelangen.

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

Reputationsstufen und Fraktionsstatus

Reputation erfasst, wie Organisationen und Fraktionen den Spieler wahrnehmen, getrennt von seinen reinen Fähigkeiten. Ein Spieler kann ein erfahrener Fahrer sein und zugleich einen schlechten Status bei der Polizei haben, weil er kriminellen Aktivitäten nachgeht. Die Reputation ist in Stufen von feindselig über neutral bis verehrt eingeteilt. Jede Stufe schaltet andere Dialogoptionen, Missionszugänge und Händlerpreise frei. Bei einer Fraktion Reputation zu gewinnen, kann sie bei verfeindeten Fraktionen verringern und so bedeutsame Abwägungen schaffen. Zum Beispiel erhöhen Drogenlieferungen die Reputation beim Kartell, senken aber die Reputation bei der Polizei. Dieses System ermutigt Spieler zu Entscheidungen, die ihren Charakter definieren, statt bei allen Fraktionen gleichzeitig den Höchstwert zu erreichen.

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

Das Prestigesystem

Sobald ein Spieler in einer Fähigkeit das Höchstlevel erreicht, braucht er einen Grund, sich weiter mit dieser Aktivität zu beschäftigen. Das Prestigesystem bietet diesen Grund: Spieler können eine Fähigkeit auf Level 1 zurücksetzen und erhalten dafür dauerhafte Boni: ein kosmetisches Prestigeabzeichen neben ihrem Namen, einen XP-Bonus von 5% pro Prestigestufe für diese Fähigkeit und Zugriff auf exklusive Prestige-Freischaltungen wie einzigartige Fahrzeugmodifikationen oder seltene Herstellungsrezepte. Der Prestigezähler ist unbegrenzt, aber jedes weitere Prestige dauert länger, weil der XP-Bonus die ersten Level trivial macht, während die Kurve auf höheren Stufen weiterhin greift. Zeige die Prestigeanzahl als römische Ziffern oder Sterne neben dem Namen der Fähigkeit in der Benutzeroberfläche an, damit das Engagement eines Spielers klar erkennbar ist.

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

Clientseitige Benutzeroberfläche und passive XP-Erfassung

Der Client erfasst passive XP für Aktivitäten wie zurückgelegte Fahrstrecken und Sprinten und sendet in regelmäßigen Abständen Updates an den Server. Ein häufiger Fehler ist, bei jedem zurückgelegten Meter ein Event zu senden, wodurch das Netzwerk überlastet wird. Sammle stattdessen die Strecke in einer lokalen Variable und sende alle 30 Sekunden ein gebündeltes Update. Erstelle für die Benutzeroberfläche ein Skill-Panel, das über einen Befehl oder eine Tastenkombination aufgerufen werden kann und alle Fähigkeiten mit aktuellem Level, XP-Fortschrittsbalken, Prestigeanzahl und Skill-Tree anzeigt. Verwende für die Baumdarstellung NUI mit einem modernen Framework wie React oder Vue. Gesperrte Knoten werden dabei ausgegraut und freigeschaltete Knoten mit einem Leuchteffekt dargestellt. Der Fortschrittsbalken sollte bei XP-Zuwachs flüssig animiert werden und so ein befriedigendes visuelles Feedback geben, das den Fortschritt verstärkt und Spieler motiviert, ihren Charakter weiterzuentwickeln.

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.