Bloga Geri Dön
Guide11 dk. okuma

FiveM İtibar Sistemi: İstatistikler, Yetenekler ve İlerleme

FiveM'e itibar ve yetenek ilerlemesi ekleyin. Değer formülleri, kilit açmalar, arayüz çubukları ve oyuncuların daha uzun süre gelişmesini sağlayacak en iyi itibar scriptleri.

Agency Scripts

Agency Scripts Kurucusu ve Baş Geliştiricisi

İtibar Sistemleri Roleplay'i Neden Dönüştürür?

Çoğu FiveM sunucusu karakter ilerlemesini ikili bir durum olarak ele alır: ya bir mesleğiniz vardır ya da yoktur. Herhangi bir gelişim, ustalık veya hak edilmiş bir statü hissi bulunmaz. İtibar ve yetenek sistemi, oyunculara harcanan zamanı ve tamamlanan aktiviteleri ödüllendiren ölçülebilir bir ilerleme sunarak bunu temelden değiştirir. Bir mekanik yüzüncü aracını tamir edip gelişmiş motor modifikasyonunun kilidini açtığında, bu başarı hak edilmiş hissettirir. Bir suçlu üst düzey soygun ekipmanlarına erişmek için yeterli sokak itibarı oluşturduğunda, oynanışını ileriye taşıyan somut bir ilerleme hissi oluşur. Bu rehberde, XP hesaplamalarını, yetenek ağaçlarını, itibar seviyelerini, kilidi açılabilir yetenekleri ve oyun sonu oyuncularının bağlılığını koruyan bir prestij sistemini kapsayan eksiksiz bir itibar ve yetenek altyapısını sıfırdan inşa edeceğiz.

Oyuncu Becerileri İçin Veri Tabanı Şeması

Herhangi bir ilerleme sisteminin temeli, iyi tasarlanmış bir veri tabanı şemasıdır. Bireysel yetenek XP'sini, fraksiyon veya etkinlik başına itibar puanlarını ve açılan yetenekleri takip eden tablolara ihtiyacınız vardır. Şema sorgulanabilir olacak kadar normalize edilmiş, ancak her yetenek kontrolünde maliyetli join işlemlerinden kaçınacak kadar denormalize edilmiş olmalıdır. Tek bir player_skills citizen ID ve yetenek adından oluşan bileşik anahtarlı tablo ile ayrı bir player_reputation fraksiyon konumu için tablo. Bu ayrım, yetenekleri (kişisel kabiliyet) itibardan (NPC'lerin ve fraksiyonların sizi nasıl algıladığı) ayrı tutarak daha ayrıntılı oynanış etkileşimleri sağlar.

-- 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 Hesaplaması ve Seviye Atlama Eğrileri

Her seviyenin aynı miktarda deneyim gerektirdiği düz bir XP eğrisi tatmin edici hissettirmez, çünkü ilk seviyeler çok hızlı geçerken sonraki seviyeler birbirinin aynısı gibi hissettirir. En iyi yaklaşım, her seviyenin giderek daha fazla XP gerektirdiği ancak artışın maksimum seviyeyi ulaşılamaz kılacak kadar dik olmadığı bir polinom eğrisi kullanmaktır. Yaygın bir formül şudur: requiredXP = baseXP * (level ^ exponent) 100 tabanı ve 1.5 üssünün pürüzsüz bir eğri oluşturduğu yer. Seviye 1 için 100 XP, seviye 10 için yaklaşık 3,162 XP ve seviye 50 için yaklaşık 35,355 XP gerekir. Bu durum, erken ilerlemeyi akıcı tutarken üst seviyelerin ulaşılması için ciddi emek gerektiren gerçek birer başarı gibi hissettirmesini sağlar.

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

Sunucu Taraflı XP Yönetimi

Açıkları önlemek için tüm XP değişiklikleri sunucu tarafında gerçekleşmelidir. Sunucu her XP verme isteğini doğrular, spam'i önlemek için hız sınırlamasını (rate limiting) denetler, aktif çarpanları uygular ve sonucu veritabanına kaydeder. Kritik bir tasarım kararı, her XP değişiminde mi kaydedileceği yoksa toplu yazma mı yapılacağıdır. Her küçük XP artışında kaydetmek, özellikle sürüş mesafesi gibi pasif yeteneklerde veritabanını aşırı zorlar. Çözüm, seviye atlama gibi önemli olaylarda anında kaydetmeyle birleştirilmiş, periyodik olarak veritabanına aktarılan bir bellek içi önbellektir. Bu, veritabanı yükünü yönetilebilir tutarken oyuncu için gerçek zamanlı doğruluk sağlar.

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

Yetenek Ağaçları ve Açılabilir Yetenekler

Yetenek ağaçları, düz seviye atlamanın üzerine stratejik bir katman ekler. Mekaniklikte 20. seviyedeki her oyuncunun aynı olması yerine, yetenek ağaçları uzmanlaşmalarını sağlar. Bir tamirci puanlarını motor performansına yatırıp turbo kiti ekleme yeteneğinin kilidini açabilirken, bir diğeri kaportada uzmanlaşarak özel boya ve kaplamalara erişim kazanabilir. Her seviye, dallanan bir ağaçta harcanabilecek bir yetenek puanı verir. Ağaç yapısı, bir alt düğümün kilidini açmak için hem üst düğümün kilidinin açılmasını hem de minimum bir yetenek seviyesini gerektiren basit bir ebeveyn-çocuk ilişkisi kullanır. Bu durum, oyuncuların temel yeterlilik kazanmadan doğrudan en güçlü yeteneklere yönelmesini önler.

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

İtibar Kademeleri ve Fraksiyon Durumu

İtibar, oyuncunun ham yeteneğinden bağımsız olarak kuruluşların ve fraksiyonların oyuncuyu nasıl gördüğünü takip eder. Bir oyuncu uzman bir sürücü olabilir ancak yasadışı faaliyetleri nedeniyle polis fraksiyonuyla arası çok kötü olabilir. İtibar: düşmandan nötre ve saygına kadar uzanan kademelere ayrılmıştır, her kademe farklı diyalog seçenekleri, görev erişimleri ve satıcı fiyatları açar. Bir fraksiyonda itibar kazanmak, karşıt fraksiyonlarda itibarı düşürebilir ve anlamlı tercihler yaratır. Örneğin uyuşturucu teslimatlarını tamamlamak kartel itibarını artırırken polis itibarını düşürür. Bu sistem, oyuncuları her fraksiyonda aynı anda seviye kasmak yerine karakterlerini tanımlayan seçimler yapmaya teşvik eder.

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

Prestij Sistemi

Bir oyuncu bir yetenekte maksimum seviyeye ulaştığında, bu aktiviteyle ilgilenmeye devam etmesi için bir nedene ihtiyaç duyar. Prestij sistemi, kalıcı bonuslar karşılığında oyuncuların bir yeteneği tekrar 1. seviyeye sıfırlamasına olanak tanıyarak bunu sağlar: İsimlerinin yanında gösterilen kozmetik bir prestij rozeti, o yetenekte prestij seviyesi başına %5 XP artışı ve benzersiz araç modifikasyonları veya nadir üretim tarifleri gibi prestije özel kilit açma haklarına erişim. Prestij sayacı sınırsızdır; ancak birbirini izleyen her prestij daha uzun sürer, çünkü XP artışı ilk seviyeleri önemsizleştirirken zorluk eğrisi üst seviyelerde farkı kapatır. Oyuncunun bağlılığını net bir şekilde göstermek için arayüzde yetenek adının yanında prestij sayısını roma rakamları veya yıldızlar olarak görüntüleyin.

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

Client Tarafı Kullanıcı Arayüzü ve Pasif XP Takibi

İstemci; sürüş mesafesi ve depar atma gibi aktiviteler için pasif XP takibini yönetir ve sunucuya periyodik güncellemeler gönderir. Yaygın bir hata, katedilen her metrede ağ trafiğini aşırı yükleyen bir etkinlik göndermektir. Bunun yerine mesafeyi yerel bir değişkende biriktirin ve her 30 saniyede bir toplu güncelleme gönderin. UI için, tüm becerileri mevcut seviyeleri, XP ilerleme çubuğu, prestij sayısı ve beceri ağacıyla birlikte gösteren, bir komut veya tuş ataması ile erişilebilen bir beceri paneli oluşturun. Ağaç görselleştirmesi için React veya Vue gibi modern bir çatıya sahip NUI kullanın; kilitli düğümleri grileştirilmiş, açık düğümleri ise ışıltı efektiyle gösterin. İlerleme çubuğu XP kazanıldığında akıcı bir şekilde canlanmalı, ilerleme döngüsünü pekiştiren ve oyuncuların karakterlerini geliştirmeye devam etmeleri için motive kalmasını sağlayan tatmin edici bir görsel geri bildirim sunmalıdır.

Başlamaya Hazır mısınız?

Scriptleri mağazamızdan edinin veya destek, güncellemeler ve gelecek yeniliklere göz atmak için Discord'a katılın.