EXOTICA NETWORKDocs

Referencia completa de configuración

Archivos editables, valores de fábrica y esquema SQL del paquete

Consulta los valores de fábrica antes de comparar tu configuración. Modifica tu copia instalada y conserva los archivos originales como respaldo. Los ejemplos de identificadores y webhooks deben sustituirse en el servidor.

config/config.lua

config/config.lua
--[[
    EN-Arenas · Configuración
    © EXOTICANETWORK.STORE — Licencia propietaria. Ver LICENSE.md.

    Este archivo se carga SOLO en el servidor: los clientes nunca lo descargan.
    Lo que el cliente necesita (posiciones de NPC, ajustes de la partida, etc.)
    se lo envía el servidor en tiempo de ejecución.
]]

Config = {}

-- Muestra mensajes de diagnóstico en la consola del servidor.
Config.Debug = false

-- 'auto', 'esx' o 'qbcore'.
Config.Framework = 'auto'

-- Comandos de chat (se registran en el servidor). Pon `open = false` para que
-- el menú solo se pueda abrir desde los NPC de lobby.
Config.Commands = {
    open = 'arenas',
    leave = 'salirarena',
    toggle = 'togglearenas'  -- solo administradores (ver Config.AdminAce)
}

-- Permiso ACE para comandos de administración. Ver permissions.cfg.
Config.AdminAce = 'en-arenas.admin'

-- Imagen y título que muestra la interfaz. El logo es una ruta dentro de web/.
Config.Branding = {
    title = 'Arenas PvP',
    logo = 'assets/logo.png'
}

-- Avatares de Discord en el marcador y el feed de bajas. El token del bot NO
-- va aquí: defínelo en server.cfg con
--     set en_arenas_discord_token "TU_TOKEN"
-- (usa `set`, nunca `setr` ni `sets`, para que no se replique a los clientes).
Config.Discord = {
    avatars = true
}

-- Sistema de ambulancia. Ver docs/INTEGRACIONES.md.
--   'auto'             detecta el que esté iniciado (Wasabi → esx_ambulancejob)
--   'wasabi'           Wasabi Ambulance (V1 y V2)
--   'esx_ambulancejob' ambulancia oficial de ESX
--   'custom'           tu propio adaptador: server/bridge/medical/custom.lua
--   'none'             sin integración médica
Config.Medical = {
    system = 'auto',
    -- Cambia el nombre si tu recurso de ambulancia está renombrado.
    resources = {
        wasabi = 'wasabi_ambulance',
        esx_ambulancejob = 'esx_ambulancejob'
    },
    blockDeadPlayers = true,        -- impide entrar estando abatido o muerto
    clearInjuriesEveryRound = true, -- cura completa al empezar cada ronda
    clearInjuriesOnExit = true      -- cura completa al salir de la arena
}

-- Apariencia real del top 3 en los NPC de podio. Solo 'illenium-appearance'
-- está soportado; pon false para usar siempre el modelo genérico del NPC.
Config.Appearance = {
    resource = 'illenium-appearance'
}

-- NPC de acceso al menú. Puedes agregar tantos como quieras.
Config.Lobbies = {
    {
        model = 'g_m_y_salvaboss_01',
        coords = vector4(-426.16, 1121.43, 325.85, 346.48),
        blip = { enabled = true, sprite = 437, colour = 27, scale = 0.8, label = 'Arenas PvP' }
    }
}

-- Distancia de interacción con los NPC de lobby (ox_target).
Config.InteractDistance = 2.0

-- Placa de rango sobre la cabeza cerca de los lobbies.
Config.RankTag = {
    enabled = true,
    showPlayers = false, -- true = también sobre los jugadores, no solo los NPC del podio
    radius = 30.0,
    heightOffset = 1.05,
    scale = 0.62
}

-- NPC de podio con el top 3. kind = 'kills' (bajas) o 'ranked' (puntos).
Config.Leaderboard = {
    enabled = true,
    refreshSeconds = 300,
    drawDistance = 12.0,
    peds = {
        { top = 1, kind = 'kills', model = 'a_m_y_business_01', coords = vector4(-406.93, 1126.06, 327.88, 78.64), scenario = 'WORLD_HUMAN_GUARD_STAND' },
        { top = 2, kind = 'kills', model = 'a_m_y_business_01', coords = vector4(-406.52, 1128.16, 326.93, 77.73), scenario = 'WORLD_HUMAN_GUARD_STAND' },
        { top = 3, kind = 'kills', model = 'a_m_y_business_01', coords = vector4(-407.58, 1123.84, 326.91, 75.07), scenario = 'WORLD_HUMAN_GUARD_STAND' }
    }
}

Config.Match = {
    roundsToWin = 3,          -- valor por defecto de las salas
    roundsMin = 1,            -- rango permitido al crear una sala
    roundsMax = 999,
    roundSeconds = 90,
    countdownSeconds = 3,     -- cuenta atrás antes de cada ronda
    intermissionSeconds = 3,  -- pausa entre rondas
    preparationSeconds = 15,  -- tiempo máximo para que todos estén listos
    podiumMs = 6000,          -- duración del podio final antes de volver al lobby
    health = 200,
    armour = 0,
    boundaryPadding = 10.0,   -- metros extra sobre maxDistance antes de devolver al spawn
    bucketBase = 4000,        -- rango de routing buckets reservados para partidas
    bucketMax = 9999,
    leaveAwardsWin = true,    -- rendirse da la victoria al equipo rival
    infiniteStamina = true,
    requireManualTeams = true, -- el dueño de la sala asigna cada jugador a un equipo
    keepWeaponInHand = true,   -- re-entrega el arma si otro recurso la quita
    enableSpectate = true,     -- al morir, cámara sobre un compañero vivo (Q/E cambia)
    assistWindowSeconds = 10,  -- ventana para contar asistencias
    tradeWindowSeconds = 2     -- un jugador recién caído aún puntúa la baja (intercambio)
}

-- Contorno sobre los compañeros, visible a través de paredes.
Config.TeamOutline = {
    enabled = true,
    duringPreparation = true,
    color = { r = 255, g = 255, b = 255, a = 255 },
    shader = 1,
    technique = 'waterreflection',
    maxDistance = 250.0,
    updateInterval = 250
}

Config.Ranked = {
    enabled = true,
    roundsToWin = 3,
    placementMatches = 10, -- partidas necesarias antes de mostrar división
    -- Puntos por tipo de partida. El rendimiento (bajas/asistencias) se suma
    -- hasta maxPerformance para que una partida larga no rompa la economía.
    scoring = {
        custom = { win = 8,  loss = -6,  draw = 1, kill = 1, assist = 1, maxPerformance = 4 },
        ranked = { win = 22, loss = -16, draw = 2, kill = 2, assist = 1, maxPerformance = 8 },
        duel   = { win = 6,  loss = -5,  draw = 0, kill = 1, assist = 0, maxPerformance = 3 }
    },
    -- Divisiones ordenadas de menor a mayor.
    tiers = {
        { id = 'semilla_criolla',     label = 'Semilla Criolla',         min = 0,     color = '#89939f' },
        { id = 'chamo_novato',        label = 'Chamo Novato',            min = 100,   color = '#929daa' },
        { id = 'arepa_de_hierro',     label = 'Arepa de Hierro',         min = 250,   color = '#9ca8b5' },
        { id = 'guarapo_bronce',      label = 'Guarapo de Bronce',       min = 450,   color = '#aa7650' },
        { id = 'motorizado_criollo',  label = 'Motorizado Criollo',      min = 700,   color = '#bd814f' },
        { id = 'guacamaya_roja',      label = 'Guacamaya Roja',          min = 1000,  color = '#d0574d' },
        { id = 'turpial_de_plata',    label = 'Turpial de Plata',        min = 1400,  color = '#c8d4df' },
        { id = 'araguaney_dorado',    label = 'Araguaney Dorado',        min = 1850,  color = '#e7b936' },
        { id = 'guardian_del_barrio', label = 'Guardián del Barrio',     min = 2350,  color = '#e2a12f' },
        { id = 'guerrero_del_caribe', label = 'Guerrero del Caribe',     min = 2900,  color = '#27b8d2' },
        { id = 'relampago_catatumbo', label = 'Relámpago del Catatumbo', min = 3500,  color = '#2d9cf0' },
        { id = 'vigia_del_avila',     label = 'Vigía del Ávila',         min = 4200,  color = '#37b986' },
        { id = 'llanero_de_acero',    label = 'Llanero de Acero',        min = 5000,  color = '#7ab1c9' },
        { id = 'rastro_del_orinoco',  label = 'Rastro del Orinoco',      min = 5900,  color = '#2c9fb4' },
        { id = 'cazador_de_tepuy',    label = 'Cazador de Tepuy',        min = 6900,  color = '#48c4a2' },
        { id = 'furia_maracucha',     label = 'Furia Maracucha',         min = 8000,  color = '#ff7b36' },
        { id = 'acero_guaro',         label = 'Acero Guaro',             min = 9200,  color = '#cf7746' },
        { id = 'sombra_oriental',     label = 'Sombra Oriental',         min = 10500, color = '#8b79d7' },
        { id = 'centinela_andino',    label = 'Centinela Andino',        min = 11900, color = '#759cd8' },
        { id = 'titan_de_la_sabana',  label = 'Titán de la Sabana',      min = 13400, color = '#45c17d' },
        { id = 'cacique_apure',       label = 'Cacique de Apure',        min = 15000, color = '#d18b3d' },
        { id = 'cacique_urdaneta',    label = 'Cacique Urdaneta',        min = 16700, color = '#de7448' },
        { id = 'cacique_tamanaco',    label = 'Cacique Tamanaco',        min = 18500, color = '#e35d51' },
        { id = 'cacique_guaicaipuro', label = 'Cacique Guaicaipuro',     min = 20400, color = '#ec4f55' },
        { id = 'centauro_del_llano',  label = 'Centauro del Llano',      min = 22400, color = '#d9b640' },
        { id = 'heroe_vinotinto',     label = 'Héroe Vinotinto',         min = 24500, color = '#b83f55' },
        { id = 'leyenda_del_salto',   label = 'Leyenda del Salto Ángel', min = 26700, color = '#42cde6' },
        { id = 'gloria_libertadora',  label = 'Gloria Libertadora',      min = 29000, color = '#f1c94a' },
        { id = 'soberano_criollo',    label = 'Soberano Criollo',        min = 31400, color = '#ff6c39' },
        { id = 'inmortal_venezuela',  label = 'Inmortal de Venezuela',   min = 34000, color = '#ff354f' }
    }
}

Config.Bets = {
    enabled = true,
    account = 'money',   -- ESX: money/bank · QBCore: cash/bank
    minimum = 0,
    maximum = 100000,
    itemsEnabled = true, -- permite apostar objetos de ox_inventory (de Config.Weapons)
    itemMaxAmount = 5
}

-- Reto 1v1 apuntando a otro jugador con ox_target dentro de una zona de duelo.
Config.Duel = {
    enabled = true,
    weapon = 'WEAPON_PISTOL_MK2',
    roundsToWin = 5,
    requestTimeoutSeconds = 15,
    targetDistance = 5.0,
    -- Claves de Config.Maps permitidas para duelos. Vacío = cualquier mapa.
    maps = { 'banco' },
    zones = {
        { coords = vector3(-273.0052, -1907.5061, 27.7554), radius = 45.0 }
    }
}

-- Capacidades disponibles (jugadores totales por partida).
Config.Modes = {
    { value = 2,  label = '1 vs 1' },
    { value = 4,  label = '2 vs 2' },
    { value = 6,  label = '3 vs 3' },
    { value = 8,  label = '4 vs 4' },
    { value = 10, label = '5 vs 5' },
    { value = 12, label = '6 vs 6' }
}

Config.Weapons = {
    { value = 'WEAPON_PISTOL',       label = 'Pistola',            category = 'Pistolas' },
    { value = 'WEAPON_PISTOL_MK2',   label = 'Pistola MK II',      category = 'Pistolas' },
    { value = 'WEAPON_COMBATPISTOL', label = 'Pistola de combate', category = 'Pistolas' },
    { value = 'WEAPON_PISTOL50',     label = 'Pistola .50',        category = 'Pistolas' },
    { value = 'WEAPON_COMBATPDW',    label = 'PDW de combate',     category = 'Subfusiles' },
    { value = 'WEAPON_MICROSMG',     label = 'Micro SMG',          category = 'Subfusiles' },
    { value = 'WEAPON_SMG',          label = 'SMG',                category = 'Subfusiles' },
    { value = 'WEAPON_CARBINERIFLE', label = 'Carabina',           category = 'Fusiles' },
    { value = 'WEAPON_COMPACTRIFLE', label = 'Rifle compacto',     category = 'Fusiles' }
}

-- Arma de la cola competitiva.
Config.RankedWeapon = 'WEAPON_PISTOL_MK2'

--[[
    Mapas. Cada uno necesita:
      label        nombre visible
      img          imagen (URL o ruta dentro de web/); opcional
      teamA/teamB  spawn de cada equipo (vector4)
      center       centro del área jugable
      maxDistance  radio permitido desde center (más Config.Match.boundaryPadding)
      ranked       false para excluirlo de la cola competitiva
]]
-- Ubicaciones del mapa base de GTA V: no necesitan ningún mapeo (ymap/MLO).
-- Spawns tomados del script gratuito fivem-paintball (BesTKiLLeR4050).
Config.Maps = {
    ['rascacielos'] = {
        label = 'Rascacielos',  -- obra del Mile High Club, último piso
        teamA = vector4(-168.86, -1011.97, 254.13, 341.67),
        teamB = vector4(-139.52, -952.93, 254.13, 159.49),
        center = vector3(-154.19, -982.45, 254.13),
        maxDistance = 35.0
    },
    ['altruistas'] = {
        label = 'Campamento Altruista',  -- Monte Chiliad
        teamA = vector4(-1022.21, 4937.08, 200.93, 143.32),
        teamB = vector4(-1093.72, 4942.15, 218.33, 158.38),
        center = vector3(-1062.0, 4925.0, 210.0),
        maxDistance = 45.0
    },
    ['banco'] = {
        label = 'Banco Pacific Standard',
        teamA = vector4(244.43, 202.98, 105.21, 73.86),
        teamB = vector4(254.34, 225.39, 106.29, 163.04),
        center = vector3(249.33, 217.76, 105.75),
        maxDistance = 30.0
    }
}

config/locale.lua

config/locale.lua
--[[
    EN-Arenas · Textos
    © EXOTICANETWORK.STORE — Licencia propietaria. Ver LICENSE.md.

    Todos los mensajes que ven los jugadores. Se cargan solo en el servidor.
    Los valores con %s / %d se completan en tiempo de ejecución.
]]

Locale = {
    -- General
    notify_title         = 'Arenas PvP',
    disabled             = 'Las arenas están desactivadas temporalmente.',
    busy                 = 'Ya estás en una sala, cola o partida.',
    invalid              = 'Solicitud inválida.',
    too_fast             = 'Vas demasiado rápido. Intenta de nuevo.',
    must_be_alive        = 'Debes estar vivo y sin estado de abatido.',
    menu_in_match        = 'No puedes abrir el menú durante una partida.',
    menu_dead            = 'No puedes abrir las arenas estando abatido o muerto.',
    menu_too_far         = 'Acércate a un punto de arenas para abrir el menú.',
    not_in_match         = 'No estás dentro de una partida.',
    toggled_on           = 'Arenas activadas.',
    toggled_off          = 'Arenas desactivadas.',

    -- Interacción
    target_open          = 'Abrir Arenas PvP',
    target_duel          = 'Duelo',
    suggestion_open      = 'Abrir el menú de arenas',
    suggestion_leave     = 'Abandonar la partida actual',

    -- Apuestas
    no_money             = 'No tienes suficiente dinero para la apuesta.',
    no_items             = 'No tienes suficientes objetos para la apuesta.',

    -- Salas
    lobby_not_found      = 'La sala no existe.',
    lobby_full           = 'La sala está llena.',
    lobby_closed         = 'El dueño cerró la sala.',
    lobby_kicked         = 'Fuiste expulsado de la sala.',
    owner_only_teams     = 'Solo el dueño asigna equipos.',
    owner_only_start     = 'Solo el dueño puede iniciar.',
    team_full            = 'Ese equipo está completo.',
    teams_unassigned     = 'Hay jugadores sin equipo asignado.',
    teams_need_both      = 'Cada equipo necesita al menos un jugador.',
    teams_overflow       = 'Hay demasiados jugadores en un equipo.',
    need_two_players     = 'Se necesitan al menos 2 jugadores.',
    start_failed         = 'No se pudo iniciar la partida; la sala fue cancelada.',

    -- Cola competitiva
    queue_disabled       = 'La cola competitiva está desactivada.',
    queue_requeued       = 'Un jugador no estaba listo. Sigues en la cola.',

    -- Duelos
    duel_target_busy     = 'Ese jugador ya está ocupado.',
    duel_pending         = 'Ese jugador ya tiene un reto pendiente.',
    duel_zone            = 'Tienes que estar en una zona de duelo.',
    duel_sent            = 'Reto de duelo enviado a %s.',
    duel_declined        = '%s rechazó tu duelo.',
    duel_unavailable     = 'El duelo ya no está disponible.',
    duel_failed          = 'No se pudo iniciar el duelo (revisa tu estado de salud).',
    duel_expired         = 'Tu reto de duelo expiró.',

    -- Partida
    not_ready            = 'No puedes entrar a una arena estando herido, abatido o muerto.',
    setup_interrupted    = 'Partida interrumpida durante la preparación. Marcador final %d–%d.',
    out_of_bounds        = 'Saliste del límite de la arena.',
    round_fight          = 'Ronda %d: ¡a pelear!',
    round_draw           = 'Ronda empatada.',
    round_won            = 'Tu equipo ganó la ronda.',
    round_lost           = 'Tu equipo perdió la ronda.',
    match_draw           = 'La partida terminó en empate. %s',
    match_won            = '¡Victoria! %s',
    match_lost           = 'Derrota. %s',
    match_abandoned      = 'La partida terminó por abandono. %s',
    points               = '%+d pts'
}

sql/install.sql

sql/install.sql
-- ---------------------------------------------------------------------------
-- EN-Arenas · Instalación de base de datos
-- © EXOTICANETWORK.STORE — Licencia propietaria. Ver LICENSE.md.
--
-- Compatible con MySQL 5.7+ y MariaDB 10.3+.
-- El recurso crea esta tabla automáticamente al iniciar; este archivo es para
-- quien prefiera instalarla a mano antes del primer arranque.
-- ---------------------------------------------------------------------------

CREATE TABLE IF NOT EXISTS `en_arenas_stats` (
    `identifier`     VARCHAR(80)  NOT NULL                COMMENT 'Identificador del framework (ESX identifier / QBCore citizenid / license)',
    `player_name`    VARCHAR(100) NOT NULL DEFAULT ''     COMMENT 'Último nombre conocido del jugador',
    `kills`          INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Bajas totales',
    `deaths`         INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Muertes totales',
    `wins`           INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Partidas ganadas',
    `losses`         INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Partidas perdidas',
    `score`          INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Puntos de clasificación',
    `matches_played` INT UNSIGNED NOT NULL DEFAULT 0      COMMENT 'Partidas jugadas (cuentan para la colocación)',
    `created_at`     TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at`     TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`identifier`),
    KEY `idx_en_arenas_ranking` (`score`, `wins`, `kills`),
    KEY `idx_en_arenas_kills` (`kills`, `score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='EN-Arenas: estadísticas y clasificación';

server/bridge/medical/custom.lua

server/bridge/medical/custom.lua
--[[
    EN-Arenas · Adaptador médico personalizado
    © EXOTICANETWORK.STORE — Licencia propietaria. Ver LICENSE.md.

    Usa este archivo si tu servidor tiene otro sistema de ambulancia
    (qb-ambulancejob, ars_ambulancejob, osp_ambulance, uno propio...).

    1. Completa las funciones de abajo con los exports/eventos de tu ambulancia.
    2. En config/config.lua pon:  Config.Medical.system = 'custom'
       (o deja 'auto' y define `resource`: se usará si ese recurso está iniciado
       y no se detecta Wasabi ni esx_ambulancejob).
    3. En tu ambulancia, ignora la muerte mientras el jugador esté en partida:
           if LocalPlayer.state['en-arenas:inMatch'] then return end      -- cliente
           if Player(source).state['en-arenas:inMatch'] then return end   -- servidor

    Ejemplos de referencia: server/bridge/medical/wasabi.lua y esx_ambulancejob.lua.
    Este archivo no está cifrado: puedes editarlo libremente.
]]

if ENA.Halted then return end

ENA.Medical.register('custom', {
    -- Recurso que debe estar iniciado. nil = siempre disponible.
    resource = nil,

    -- true si el jugador está abatido o muerto según tu ambulancia.
    isDead = function(source)
        local state = Player(source).state
        return state.isDead == true
    end,

    -- Revive completo: limpia el estado de muerte de tu ambulancia.
    revive = function(source)
        -- Ejemplo con evento de cliente:
        -- TriggerClientEvent('mi_ambulancia:revive', source)
        --
        -- Ejemplo con export de servidor:
        -- exports['mi_ambulancia']:RevivePlayer(source)
    end,

    -- Cura total / limpieza de lesiones entre rondas (opcional).
    heal = function(source)
        -- Ejemplo con export de CLIENTE:
        -- ENA.Medical.callClientExport(source, 'mi_ambulancia', 'healPlayer', true)
    end,

    -- State bags que tu ambulancia activa al morir/abatir (opcional).
    deathStates = { 'isDead' },

    -- Cómo detectar en el cliente que el jugador sigue muerto (opcional).
    client = {
        deadStates = { 'isDead' },  -- state bags del jugador local
        deadExport = nil            -- export de cliente que devuelva true si está muerto
    }
})

En esta página