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.