Volver al blog
Guide10 min de lectura

Sistema de árbol de habilidades para FiveM: progresa de nivel como debe ser

Añade un árbol de habilidades a tu servidor FiveM. XP, desbloqueos, diseño de interfaz, empleos y scripts para lograr metas a largo plazo.

Agency Scripts

Fundador y desarrollador principal en Agency Scripts

Por qué los árboles de habilidades transforman los servidores de roleplay

La mayoría de los servidores de roleplay de FiveM se basan en una progresión por trabajos, en la que el jugador elige una carrera y obtiene acceso inmediato a todas sus herramientas y habilidades. Aunque es un enfoque funcional, no ofrece una sensación de crecimiento ni de inversión a largo plazo. Los árboles de habilidades cambian esa dinámica por completo. Al introducir una progresión al estilo RPG, los jugadores ganan experiencia con sus acciones y gastan puntos de habilidad para desbloquear habilidades, ventajas y bonificaciones pasivas en árboles de talentos ramificados. Un mecánico que repara cientos de coches se vuelve gradualmente más rápido y eficiente. Un criminal que fuerza cerraduras repetidamente mejora en esa tarea. Esto crea un desarrollo de personaje significativo que recompensa la dedicación y hace que cada personaje sea único. La clave está en diseñar un sistema que enriquezca el roleplay en lugar de convertir tu servidor en una rutina de farmeo de MMO, manteniendo las bonificaciones lo bastante moderadas para que las diferencias de habilidad complementen las situaciones de RP en vez de dominarlas.

Diseño de la estructura de datos del árbol de habilidades

Un árbol de habilidades bien diseñado parte de una estructura de datos clara. Cada árbol pertenece a una categoría, como combate, fabricación, conducción, medicina o delincuencia. Dentro de cada árbol, los nodos individuales representan habilidades que se pueden desbloquear. Cada nodo tiene un identificador único, un nombre para mostrar, una descripción, un nivel máximo, un coste de puntos por nivel, nodos previos que deben desbloquearse primero y el efecto concreto que aplica al juego. Guarda las definiciones de los árboles en un archivo de configuración compartido para que tanto el servidor como el cliente puedan consultarlas. El progreso del jugador usa una estructura de datos aparte que registra qué nodos ha desbloqueado y en qué nivel, además de los puntos de habilidad disponibles que aún no ha gastado. Separar la definición del árbol del progreso del jugador te permite actualizar o reequilibrar el árbol sin migrar los datos de los jugadores, siempre que los identificadores de los nodos se mantengan estables.

-- shared/skill_trees.lua
SkillTrees = {
    mechanic = {
        label = 'Mechanic',
        icon  = 'fa-wrench',
        nodes = {
            quickFix = {
                label    = 'Quick Fix',
                desc     = 'Reduce vehicle repair time',
                maxLevel = 5,
                cost     = 1,
                prereqs  = {},
                effect   = {type = 'repair_speed', perLevel = 0.10},
            },
            engineExpert = {
                label    = 'Engine Expert',
                desc     = 'Repair engines to higher condition',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'quickFix'},
                effect   = {type = 'engine_quality', perLevel = 0.15},
            },
            bodyworkMaster = {
                label    = 'Bodywork Master',
                desc     = 'Repair body damage more effectively',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'quickFix'},
                effect   = {type = 'body_quality', perLevel = 0.15},
            },
            turboTuner = {
                label    = 'Turbo Tuner',
                desc     = 'Unlock performance tuning abilities',
                maxLevel = 1,
                cost     = 5,
                prereqs  = {'engineExpert'},
                effect   = {type = 'unlock_tuning', perLevel = 1},
            },
        },
    },
    criminal = {
        label = 'Street Smarts',
        icon  = 'fa-mask',
        nodes = {
            lockpicking = {
                label    = 'Lockpicking',
                desc     = 'Pick locks faster with fewer failures',
                maxLevel = 5,
                cost     = 1,
                prereqs  = {},
                effect   = {type = 'lockpick_speed', perLevel = 0.12},
            },
            silentStep = {
                label    = 'Silent Step',
                desc     = 'Reduce noise while crouching',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'lockpicking'},
                effect   = {type = 'noise_reduction', perLevel = 0.20},
            },
            safecracker = {
                label    = 'Safecracker',
                desc     = 'Attempt to crack safes',
                maxLevel = 1,
                cost     = 4,
                prereqs  = {'lockpicking'},
                effect   = {type = 'unlock_safecrack', perLevel = 1},
            },
        },
    },
}

Seguimiento de experiencia y subida de nivel

La experiencia debe ganarse de forma natural mediante las acciones de juego, no con ciclos de farmeo repetitivos. Haz un seguimiento de la experiencia por categoría de árbol de habilidades, de modo que trabajar como mecánico otorgue XP de mecánica, cometer delitos otorgue XP criminal y así sucesivamente. Define los umbrales de experiencia para cada nivel y, cuando un jugador alcance uno, otórgale uno o más puntos de habilidad para gastar en ese árbol. Aplica rendimientos decrecientes a las acciones idénticas repetidas para desalentar los ciclos de explotación. Por ejemplo, la primera reparación de un vehículo en un periodo de 10 minutos podría otorgar 50 XP, pero las siguientes otorgarían 25, luego 10 y después 5. Así se recompensa la variedad de juego en lugar de la repetición mecánica. El servidor debe ser la única autoridad que otorgue XP, nunca confíes en que el cliente informe sus propios valores de experiencia. Activa los eventos de XP desde la lógica del servidor después de verificar que la acción realmente se haya producido.

-- server/experience.lua
local XP_THRESHOLDS = {0, 100, 300, 600, 1000, 1500, 2200, 3000, 4000, 5500}

local recentActions = {}

function GrantXP(playerId, tree, amount, actionType)
    local identifier = GetPlayerIdentifier(playerId, 0)
    local now = os.time()

    -- Diminishing returns for repeated actions
    local key = identifier .. ':' .. tree .. ':' .. actionType
    if not recentActions[key] then
        recentActions[key] = {count = 0, resetAt = now + 600}
    end
    local ra = recentActions[key]
    if now > ra.resetAt then
        ra.count = 0
        ra.resetAt = now + 600
    end
    ra.count = ra.count + 1

    local multiplier = math.max(0.1, 1.0 - (ra.count - 1) * 0.25)
    local finalXP = math.floor(amount * multiplier)

    -- Update database
    MySQL.update([[
        UPDATE character_skills SET xp = xp + ?
        WHERE identifier = ? AND tree = ?
    ]], {finalXP, identifier, tree})

    -- Check for level up
    local data = MySQL.single.await([[
        SELECT xp, level, points FROM character_skills
        WHERE identifier = ? AND tree = ?
    ]], {identifier, tree})

    if data then
        local nextThreshold = XP_THRESHOLDS[data.level + 2]
        if nextThreshold and data.xp >= nextThreshold then
            local newLevel = data.level + 1
            MySQL.update([[
                UPDATE character_skills
                SET level = ?, points = points + 1
                WHERE identifier = ? AND tree = ?
            ]], {newLevel, identifier, tree})
            TriggerClientEvent('skills:levelUp', playerId, tree, newLevel)
        end
    end

    TriggerClientEvent('skills:xpGained', playerId, tree, finalXP)
end

Aplicación de los efectos de las habilidades al juego

Los efectos de las habilidades son lo que hace que el sistema sea tangible para los jugadores. Cada nodo desbloqueado modifica una mecánica concreta del juego de forma cuantificable. Implementa un resolutor central de efectos que reciba el identificador de un jugador y un tipo de efecto, y después calcule la bonificación total de todos los nodos desbloqueados que contribuyen a ese efecto. Cuando otro script necesite conocer la bonificación a la velocidad de reparación de un jugador, llamará a este resolutor y obtendrá un multiplicador. Así, el sistema de habilidades queda desacoplado de otros recursos. Los demás scripts no necesitan conocer los árboles de habilidades, los nodos ni los niveles: solo solicitan un valor de bonificación por tipo. Para las habilidades desbloqueables, como abrir cajas fuertes o ajustar turbocompresores, el resolutor devuelve un valor booleano que indica si el jugador tiene el nodo requerido. Esta arquitectura facilita la integración de habilidades con los scripts existentes y requiere cambios mínimos en ellos.

-- server/effects.lua
local playerSkillCache = {}

function GetSkillBonus(playerId, effectType)
    local identifier = GetPlayerIdentifier(playerId, 0)
    local cache = playerSkillCache[identifier]
    if not cache then return 0.0 end

    local total = 0.0
    for treeName, treeDef in pairs(SkillTrees) do
        for nodeId, nodeDef in pairs(treeDef.nodes) do
            if nodeDef.effect.type == effectType then
                local unlocked = cache[treeName] and cache[treeName][nodeId] or 0
                total = total + (nodeDef.effect.perLevel * unlocked)
            end
        end
    end
    return total
end

function HasSkillUnlock(playerId, effectType)
    return GetSkillBonus(playerId, effectType) > 0
end

-- Export for other resources
exports('GetSkillBonus', GetSkillBonus)
exports('HasSkillUnlock', HasSkillUnlock)

-- Example: another resource checking repair speed
-- local bonus = exports['skillsystem']:GetSkillBonus(source, 'repair_speed')
-- local repairTime = baseTime * (1.0 - bonus)

Creación de la NUI del árbol de habilidades

La presentación visual del árbol de habilidades influye directamente en cuánto se involucran los jugadores con el sistema de progresión. Crea un panel NUI que muestre cada árbol como un gráfico ramificado con nodos conectados por líneas que indiquen los requisitos previos. Los nodos desbloqueados deben iluminarse o pulsar con color, los nodos bloqueados pero disponibles deben aparecer ligeramente atenuados y los nodos cuyos requisitos previos no se hayan cumplido deben mostrarse en gris. Muestra en la parte superior el nivel actual del jugador, la barra de progreso de XP y los puntos de habilidad disponibles. Cuando un jugador seleccione un nodo, muestra su descripción, el nivel actual respecto al máximo, el coste de mejora y la bonificación del efecto por nivel. Incluye un paso de confirmación antes de gastar puntos, ya que los jugadores quizá quieran planificar cuidadosamente sus configuraciones. Usa transiciones CSS suaves para los cambios de estado, de modo que mejorar un nodo resulte gratificante, con un breve destello o una animación de escala. Considera añadir una opción para reiniciar la configuración, gratis o a cambio de moneda del juego, para que los jugadores puedan reasignar sus puntos si cambia el meta o quieren probar otro estilo de juego.

Consideraciones de equilibrio y prevención de exploits

El equilibrio es la parte más difícil de cualquier sistema de progresión. Si las bonificaciones son demasiado fuertes, los jugadores de nivel alto se vuelven intocables y los nuevos se sienten indefensos. Si son demasiado débiles, a nadie le importa progresar. Empieza con moderación, con bonificaciones del 5 al 15 por ciento por habilidad en el nivel máximo, y haz un seguimiento de los comentarios de los jugadores. Implementa validación en el servidor para cada asignación de puntos de habilidad. Cuando un jugador solicite mejorar un nodo, verifica que tenga suficientes puntos, que se cumplan todos los requisitos previos y que el nodo no esté ya en el nivel máximo. Nunca confíes en el estado del cliente para estas comprobaciones. Registra todas las transacciones de puntos de habilidad para detectar y revertir exploits. Limita la frecuencia de las solicitudes de asignación de habilidades para prevenir intentos de explotación repetidos. En situaciones competitivas, considera establecer límites de habilidades que restrinjan la cantidad de árboles en los que puede invertir un personaje, lo que obliga a tomar decisiones significativas en lugar de permitir que un jugador lo maximice todo. Esto crea configuraciones de personaje diversas y fomenta la cooperación entre jugadores con habilidades complementarias.

Esquema de base de datos y estrategia de migración

Diseña el esquema de tu base de datos para gestionar correctamente tanto los datos de habilidades actuales como las futuras ampliaciones de los árboles. Guarda las asignaciones de habilidades de los jugadores en filas individuales por árbol y personaje, en lugar de en un único bloque JSON. Esto permite consultar habilidades específicas de forma eficiente y realizar agregaciones en la base de datos para análisis. Incluye columnas para el nombre del árbol, el nivel actual, la XP total, los puntos disponibles y una columna JSON para las asignaciones de nodos dentro de ese árbol. Cuando añadas nuevos árboles o nodos en futuras actualizaciones, los datos existentes de los jugadores se conservarán porque los nodos nuevos simplemente empezarán en nivel cero. Si renombras o eliminas un nodo, gestiona la migración devolviendo los puntos gastados al fondo de puntos disponibles de ese árbol para el jugador. Asigna versiones a las definiciones de los árboles para que la lógica de migración sepa qué cambios aplicar. Este enfoque previsivo te evitará migraciones de datos complicadas a medida que el sistema de habilidades evolucione durante los meses de funcionamiento del servidor.

-- SQL schema
CREATE TABLE IF NOT EXISTS character_skills (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    identifier  VARCHAR(64) NOT NULL,
    char_slot   INT DEFAULT 1,
    tree        VARCHAR(32) NOT NULL,
    level       INT DEFAULT 0,
    xp          INT DEFAULT 0,
    points      INT DEFAULT 0,
    allocations JSON DEFAULT '{}',
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY unique_char_tree (identifier, char_slot, tree),
    INDEX idx_identifier (identifier)
);

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.