Wróć do bloga
Guide11 min czytania

System reputacji w FiveM: statystyki, umiejętności i progresja

Dodaj reputację i progresję umiejętności do FiveM. Formuły statystyk, odblokowania, paski UI i najlepsze skrypty reputacji, aby utrzymać graczy przy grindzie na dłużej.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego systemy reputacji odmieniają Roleplay

Większość serwerów FiveM traktuje progresję postaci binarnie: albo masz pracę, albo nie. Brakuje w tym poczucia rozwoju, mistrzostwa czy wypracowanego statusu. System reputacji i umiejętności zmienia to diametralnie, dając graczom mierzalną progresję, która nagradza poświęcony czas i ukończone aktywności. Kiedy mechanik naprawia swój setny pojazd i odblokowuje zaawansowany tuning silnika, to osiągnięcie daje poczucie zasłużonego sukcesu. Kiedy przestępca buduje wystarczającą reputację na ulicy, by zyskać dostęp do zaawansowanego sprzętu do napadów, pojawia się namacalne poczucie progresji napędzające rozgrywkę. W tym poradniku zbudujemy od podstaw kompletny framework reputacji i umiejętności, obejmujący obliczenia XP, drzewka umiejętności, poziomy reputacji, umiejętności do odblokowania oraz system prestiżu, który angażuje graczy na etapie endgame.

Schemat bazy danych dla umiejętności graczy

Podstawą każdego systemu progresji jest dobrze zaprojektowany schemat bazy danych. Potrzebujesz tabel śledzących punkty XP poszczególnych umiejętności, punkty reputacji dla danej frakcji lub aktywności oraz odblokowane zdolności. Schemat powinien być na tyle znormalizowany, aby można było wykonywać zapytania, ale jednocześnie na tyle zdenormalizowany, aby uniknąć kosztownych złączeń przy każdym sprawdzaniu umiejętności. Używamy pojedynczej player_skills tabela ze złożonym kluczem składającym się z citizen ID i nazwy umiejętności, a także oddzielna player_reputation tabela dla reputacji we frakcjach. Ten podział oddziela umiejętności (zdolności osobiste) od reputacji (sposobu, w jaki postrzegają Cię NPC i frakcje), co pozwala na bardziej zróżnicowane interakcje w rozgrywce.

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

Obliczanie XP i krzywe poziomów

Płaska krzywa XP, w której każdy poziom wymaga tyle samo doświadczenia, jest mało satysfakcjonująca, ponieważ początkowe poziomy mija się zbyt szybko, a późniejsze wydają się identyczne. Najlepsze podejście wykorzystuje krzywą wielomianową, w której każdy poziom wymaga stopniowo więcej punktów XP, ale wzrost nie jest na tyle stromy, by maksymalny poziom stał się nieosiągalny. Powszechną formułą jest requiredXP = baseXP * (level ^ exponent) gdzie wartość bazowa 100 i wykładnik 1.5 tworzą łagodną krzywą. Poziom 1 wymaga 100 XP, poziom 10 wymaga około 3,162 XP, a poziom 50 w przybliżeniu 35,355 XP. Zapewnia to dynamiczny początkowy postęp, sprawiając jednocześnie, że wyższe poziomy są odczuwalne jako prawdziwe osiągnięcia, których zdobycie wymaga zaangażowania w grę.

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

Zarządzanie XP po stronie serwera

Wszystkie modyfikacje XP muszą odbywać się po stronie serwera, aby zapobiec exploitom. Serwer weryfikuje każde żądanie przyznania XP, sprawdza rate limiting, aby zapobiec spamowi, stosuje aktywne mnożniki i zapisuje wynik w bazie danych. Kluczową decyzją projektową jest to, czy zapisywać przy każdej zmianie XP, czy grupować operacje zapisu. Zapisywanie przy każdym drobnym przyroście XP mocno obciąża bazę danych, szczególnie w przypadku umiejętności pasywnych, takich jak przejechany dystans. Rozwiązaniem jest pamięć podręczna w RAM, która okresowo zrzuca dane do bazy danych, połączona z natychmiastowym zapisem przy istotnych zdarzeniach, takich jak awans na wyższy poziom. Zapewnia to graczowi dokładność w czasie rzeczywistym przy jednoczesnym zachowaniu optymalnego obciążenia bazy danych.

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

Drzewka umiejętności i odblokowywane zdolności

Drzewka umiejętności dodają element strategii do zwykłego awansowania na kolejne poziomy. Zamiast sytuacji, w której każdy gracz na 20. poziomie mechanika jest identyczny, drzewka umiejętności pozwalają im się specjalizować. Jeden mechanik może zainwestować punkty w osiągi silnika, odblokowując możliwość montażu turbosprężarek, podczas gdy inny wyspecjalizuje się w blacharstwie, zyskując dostęp do ekskluzywnych lakierów i oklein. Każdy poziom daje jeden punkt umiejętności do wydania w rozgałęzionym drzewku. Struktura drzewka opiera się na prostej relacji nadrzędny-podrzędny, w której odblokowanie węzła podrzędnego wymaga zarówno odblokowania węzła nadrzędnego, jak i minimalnego poziomu umiejętności. Zapobiega to szybkiemu odblokowywaniu najpotężniejszych zdolności bez wcześniejszego zdobycia podstawowych kompetencji.

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

Poziomy reputacji i relacje z frakcjami

Reputacja określa, jak organizacje i frakcje postrzegają gracza, niezależnie od samych umiejętności. Gracz może być wybitnym kierowcą, ale mieć fatalne relacje z policją z powodu działalności przestępczej. Reputacja jest podzielona na poziomy: od wrogiego przez neutralny aż po szanowany, a każdy z nich odblokowuje inne opcje dialogowe, dostęp do misji oraz ceny u handlarzy. Zyskiwanie reputacji u jednej frakcji może obniżać ją u frakcji przeciwnej, tworząc istotne kompromisy. Na przykład dostarczanie narkotyków zwiększa reputację w kartelu, ale obniża reputację w policji. System ten zachęca graczy do podejmowania wyborów definiujących ich postać, zamiast maksymalizowania każdej frakcji jednocześnie.

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

System prestiżu

Gdy gracz osiągnie maksymalny poziom danej umiejętności, potrzebuje powodu, by dalej się w nią angażować. Zapewnia to system prestiżu, umożliwiając zresetowanie umiejętności z powrotem do poziomu 1 w zamian za stałe bonusy: kosmetyczną odznakę prestiżu wyświetlaną obok nicku, 5% premii do XP na poziom prestiżu w danej umiejętności oraz dostęp do unikalnych nagród prestiżu, takich jak rzadkie modyfikacje pojazdów czy unikalne receptury craftingu. Licznik prestiżu jest nieograniczony, ale każdy kolejny poziom zajmuje więcej czasu, ponieważ premia do XP sprawia, że wczesne poziomy są banalne, podczas gdy krzywa trudności dogania gracza na wyższych poziomach. Wyświetlaj liczbę prestiży jako cyfry rzymskie lub gwiazdki obok nazwy umiejętności w UI, aby dać czytelny wizualny wskaźnik zaangażowania gracza.

-- 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 po stronie klienta i pasywne śledzenie XP

Klient obsługuje pasywne śledzenie XP za czynności takie jak przebyty dystans autem czy sprint, wysyłając cykliczne aktualizacje na serwer. Częstym błędem jest wysyłanie zdarzenia po każdym przebytym metrze, co zalewa sieć pakietami. Zamiast tego kumuluj dystans w zmiennej lokalnej i wysyłaj zbiorczą aktualizację co 30 sekund. W przypadku UI zbuduj panel umiejętności dostępny za pomocą komendy lub keybinda, który pokazuje wszystkie skille z ich aktualnym poziomem, paskiem postępu XP, licznikiem prestiżu oraz drzewkiem umiejętności. Użyj NUI z nowoczesnym frameworkiem, takim jak React lub Vue, do wizualizacji drzewka, pokazując zablokowane węzły jako wyszarzone, a odblokowane z efektem poświaty. Pasek postępu powinien płynnie animować się po zdobyciu XP, dając satysfakcjonujący feedback wizualny, który wzmacnia pętlę progresji i motywuje graczy do dalszego rozwijania postaci.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.