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.