DRS VIP Store

v2.1.0 drs_donations
Ver na loja

A DRS VIP Store é uma loja VIP com moeda própria (DRS Coins por defeito). Os jogadores compram coins no Tebex, resgatam o código na loja e gastam-nas no que quiseres vender:

  • veículos e aéreos com a foto oficial, as estatísticas reais e matrícula à escolha;
  • casas do qb-houses, peds VIP, VIPs ativados pela staff e prioridade na fila;
  • armas, itens, dinheiro e crypto;
  • matrícula e número de telemóvel personalizados;
  • pacotes, promoções, edições limitadas, códigos de eventos e ofertas entre jogadores.

Abre com F4 ou /loja. Funciona com QBCore e Qbox.

#Requisitos

RecursoObrigatório
oxmysqlSim
QBCore ou QboxSim
qb-housesNão — só para vender casas
connectqueueNão — só para a prioridade na fila
DRS Ped VIP (drs_pedvip)Não — só para vender peds

#Instalação

  1. Coloca drs_donations em resources.
  2. Adiciona ao server.cfg, depois do core, do inventário e do oxmysql:
server.cfg
ensure oxmysql
ensure qb-core
ensure qb-inventory
ensure drs_donations
  1. As tabelas criam-se sozinhas no primeiro arranque (também tens sql/drs_donations.sql).
  2. Em config.lua confirma o nome do core (Config.Core), do inventário (Config.Inventory) e a garagem (Config.DefaultGarage).
  3. Em server/config_server.lua põe o webhook das compras (Webhook) e o dos pedidos à staff (RequestWebhook).

#Tebex

Em cada pacote de coins do Tebex, adiciona este comando de servidor e marca "executar mesmo com o jogador offline":

Tebex → Pacote → Comandos
drs_coins {"transid":"{transaction}","coins":"1000"}

O jogador recebe o ID da transação (tbx-...) por email e resgata-o na loja, em Início > Resgatar código. Cada código só dá uma vez.

Nota

O comando antigo purchase_donate_credit {"transid":"{transaction}","credit":"1000"} também funciona, para quem vem de outra loja.

Os pacotes que aparecem na loja (coins, bónus, preço, imagem e link) estão em Config.Packs.

#Produtos

Cada produto em Config.Products tem um id (não mudar depois de haver compras), uma category, o price em coins e as rewards. Um produto pode ter várias recompensas (pacotes).

config.lua
{ id = 'starter', category = 'bundles', label = 'Pacote Iniciante', price = 900, oldPrice = 1350, tag = 'SÓ 1 VEZ', limit = 1, featured = true,
  rewards = {
      { type = 'vehicle', model = 'jugular' },
      { type = 'item', name = 'weapon_heavypistol', amount = 1 },
      { type = 'money', account = 'bank', amount = 150000 },
  } },
RecompensaO que dá
{ type = 'vehicle', model = 'krieger' }Veículo na garagem (Config.DefaultGarage)
{ type = 'item', name = 'weapon_pistol', amount = 1 }Item ou arma no inventário
{ type = 'money', account = 'bank', amount = 100000 }Dinheiro: bank, cash ou crypto
{ type = 'house', name = 'lake vinewood est1' }Casa do qb-houses com as chaves (nome da tabela houselocations)
{ type = 'ped', model = 'player_one' }Ped VIP para sempre (precisa do drs_pedvip)
{ type = 'priority', tier = 'VIP', power = 35 }Prioridade na fila do connectqueue
{ type = 'request', text = 'VIP OURO' }Pedido à staff no Discord (para o que é ativado à mão)
{ type = 'plate' }Matrícula personalizada no carro onde o jogador está
{ type = 'phone' }Número de telemóvel à escolha (random = true: número novo ao calhas)
{ type = 'keys', amount = 5 } / { type = 'skin', id = '...' }Chaves e skins das caixas da Base DRS PVP

Opções de cada produto:

  • oldPrice: preço riscado (promoção); tag: etiqueta (NOVO, HOT…); featured: aparece no Início;
  • limit: máximo de compras por jogador; availableUntil = 'AAAA-MM-DD': sai da loja depois desse dia;
  • plateOption: preço extra para escolher a matrícula ao comprar um veículo;
  • label, desc e image: nos veículos vêm do core e da galeria do FiveM; nos itens, do inventário.

As casas saem da loja quando já têm dono. Os peds também podem vir da base de dados: liga Config.PedCatalog e a loja lê a tabela vip_ped_catalog (name, price, enabled).

#Integrações por tabela

Para ligar a loja a outros scripts há mais três recompensas, que só escrevem numa tabela (o teu script tem de a ler):

  • { type = 'charslots', amount = 1 } → vip_character_slots (personagens extra no multicharacter);
  • { type = 'housestash', slots = 10, weight = 200000 } → vip_house_stash_upgrade (baú da casa onde o jogador está);
  • { type = 'salary', amount = 2000, days = 30 } → vip_salary e vip_players (salário VIP).

#Segurança

  • O servidor decide tudo: preço, saldo, limites e o que se recebe. A interface só pede.
  • O saldo desconta numa única operação: duas compras ao mesmo tempo não passam do saldo.
  • Se uma recompensa falhar (inventário cheio, casa já vendida, ped que não aplica), as coins voltam e nada fica dado a meio.
  • Os códigos têm proteção contra quem tenta adivinhar (ServerConfig.RedeemTries).
  • O dinheiro passa pelo anti-cheat do servidor quando existe (drake_guard).

#Comandos de staff (ACE)

ComandoO que faz
/darcoins <id> <n> · /tirarcoins <id> <n>Dar ou tirar coins
/vercoins <id>Ver o saldo de um jogador
`/criarcodigocoins <CODIGO\auto> <coins> [usos] [dias]`Código para eventos (0 usos = sem limite)
/apagarcodigocoins <codigo>Apagar um código
server.cfg
add_ace group.admin command.darcoins allow
add_ace group.admin command.tirarcoins allow
add_ace group.admin command.vercoins allow
add_ace group.admin command.criarcodigocoins allow
add_ace group.admin command.apagarcodigocoins allow

#Para programadores

servidor
exports.drs_donations:GetCoins(source)
exports.drs_donations:AddCoins(source, 500, 'Evento de Halloween')
exports.drs_donations:RemoveCoins(source, 100)
cliente
exports.drs_donations:Open()   -- abre a loja (ex.: a partir de outro menu)

#Configuração completa

config.lua
-- =====================================================================
--  DRS DONATIONS: loja VIP com moeda própria (DRS Coins)
--  Os jogadores compram coins no Tebex, resgatam o código da compra na loja (F4) e gastam as coins em veículos,
--  casas, peds, VIPs, prioridade na fila, armas, dinheiro, matrículas e números de telemóvel.
--  O webhook do Discord fica em server/config_server.lua (não é enviado aos jogadores).
-- =====================================================================
Config = {}

Config.Core = 'qb-core'                -- nome do recurso do core (QBCore; no Qbox também 'qb-core')
Config.Inventory = 'qb-inventory'      -- inventário (AddItem / RemoveItem e imagens dos itens)
Config.Command = 'loja'                -- /loja abre a loja (também /donate)
Config.Key = 'F4'                      -- tecla da loja (cada jogador pode mudar em Definições > Teclas)
Config.ServerName = 'DRS'              -- aparece no topo da loja
Config.Currency = { name = 'DRS Coins', short = 'DC' }
Config.PerAccount = true               -- true = as coins são da conta (licença, todas as personagens); false = de cada personagem
Config.DefaultGarage = 'pillboxgarage' -- garagem para onde vão os veículos comprados
Config.Phone = 'qb-phone'              -- o número fica em charinfo.phone (e no drs_phone, se o usares)
Config.KeysEvent = 'vehiclekeys:client:SetOwner' -- evento que dá as chaves da matrícula nova
Config.TebexUrl = 'https://drs.tebex.io' -- botão "Loja Tebex" (os pacotes abaixo têm o link de cada um)

-- oferecer coins a outro jogador (em jogo, pelo ID)
Config.Gifts = { enabled = true, min = 50, cooldown = 30 } -- cooldown em segundos

-- matrícula e número de telemóvel personalizados
Config.Plate = { min = 2, max = 8 }            -- letras / números (A-Z, 0-9 e espaço)
Config.PhoneNumber = { min = 7, max = 9 }      -- só números

-- ------------------------------------------------------------------ pacotes de coins (Tebex)
-- No Tebex, em cada pacote, adiciona o comando de servidor (marca "executar mesmo com o jogador offline"):
--   drs_coins {"transid":"{transaction}","coins":"1000"}
-- O jogador recebe o ID da transação (tbx-...) por email e resgata-o na loja (Início > Resgatar código).
-- (O comando antigo purchase_donate_credit {"transid":"{transaction}","credit":"1000"} continua a funcionar.)
Config.Packs = {
    { coins = 500,  bonus = 0,    price = '5€',  image = 'img/pack1.png', url = 'https://drs.tebex.io' },
    { coins = 1000, bonus = 100,  price = '10€', image = 'img/pack2.png', url = 'https://drs.tebex.io', tag = 'POPULAR' },
    { coins = 2500, bonus = 400,  price = '25€', image = 'img/pack3.png', url = 'https://drs.tebex.io' },
    { coins = 5000, bonus = 1250, price = '50€', image = 'img/pack4.png', url = 'https://drs.tebex.io', tag = 'MELHOR VALOR' },
}

-- ------------------------------------------------------------------ categorias (pela ordem do menu)
Config.Categories = {
    { id = 'vehicles', label = 'Veículos', icon = 'fa-car-side' },
    { id = 'houses', label = 'Casas', icon = 'fa-house' },
    { id = 'peds', label = 'Personagens', icon = 'fa-user-astronaut' },
    { id = 'bundles', label = 'Pacotes VIP', icon = 'fa-crown' },
    { id = 'air', label = 'Aéreos', icon = 'fa-helicopter' },
    { id = 'weapons', label = 'Armas', icon = 'fa-gun' },
    { id = 'money', label = 'Dinheiro', icon = 'fa-sack-dollar' },
    { id = 'cases', label = 'Caixas', icon = 'fa-box-open' },
    { id = 'extras', label = 'Personalizar', icon = 'fa-wand-magic-sparkles' },
}

-- ------------------------------------------------------------------ produtos
-- id único (não mudar depois de haver compras) · category · price em coins
-- label / desc / image: opcionais nos veículos (vêm do drs-core e da galeria do FiveM)
-- oldPrice = preço riscado (promoção) · tag = etiqueta (NOVO, HOT...) · featured = aparece no Início
-- limit = máximo de compras por conta (nil = sem limite)
-- rewards = o que recebe:
--   { type = 'vehicle', model = 'krieger' }              veículo na garagem (Config.DefaultGarage)
--   { type = 'item', name = 'weapon_pistol', amount = 1 } item / arma no inventário
--   { type = 'money', account = 'bank', amount = 100000 }
--   { type = 'keys', amount = 5 }                         chaves das caixas da Base DRS PVP (drs_pvp)
--   { type = 'skin', id = 'pistol_luxe' }                 skin de arma da Base DRS PVP (ids no drs_pvp/config_cases.lua)
--   { type = 'plate' }                                    matrícula personalizada no carro onde está (tem de ser dele)
--   { type = 'phone' }                                    número de telemóvel personalizado ({ type = 'phone', random = true } = número novo ao calhas)
--   { type = 'house', name = 'nome em houselocations' }   casa do qb-houses (sai da loja quando tem dono)
--   { type = 'ped', model = 'nome do ped' }               ped VIP para sempre (precisa do DRS Ped VIP: drs_pedvip)
--   { type = 'priority', tier = 'VIP', power = 35 }       prioridade na fila do connectqueue (fica guardada)
--   { type = 'request', text = 'VIP OURO' }               a staff ativa à mão: chega um pedido ao Discord (ServerConfig.RequestWebhook)
--   Integrações (o teu script tem de ler estas tabelas): { type = 'charslots', amount = 1 } -> vip_character_slots,
--   { type = 'housestash', slots = 10, weight = 200000 } -> vip_house_stash_upgrade (casa onde o jogador está),
--   { type = 'salary', amount = 2000, days = 30 } -> vip_salary / vip_players
-- availableUntil = 'AAAA-MM-DD' -> o produto sai da loja depois desse dia (edições limitadas)
-- plateOption = preço extra para escolher a matrícula ao comprar um veículo (nil = não dá)
Config.Products = {
    -- veículos
    { id = 'krieger', category = 'vehicles', price = 2400, featured = true, tag = 'HOT', plateOption = 150, rewards = { { type = 'vehicle', model = 'krieger' } } },
    { id = 'emerus', category = 'vehicles', price = 2200, plateOption = 150, rewards = { { type = 'vehicle', model = 'emerus' } } },
    { id = 'zentorno', category = 'vehicles', price = 1800, oldPrice = 2200, image = 'img/zentorno.png', plateOption = 150, rewards = { { type = 'vehicle', model = 'zentorno' } } },
    { id = 't20', category = 'vehicles', price = 1900, plateOption = 150, rewards = { { type = 'vehicle', model = 't20' } } },
    { id = 'deveste', category = 'vehicles', price = 2600, tag = 'NOVO', plateOption = 150, rewards = { { type = 'vehicle', model = 'deveste' } } },
    { id = 'italigto', category = 'vehicles', price = 1500, plateOption = 150, rewards = { { type = 'vehicle', model = 'italigto' } } },
    { id = 'jugular', category = 'vehicles', price = 1200, plateOption = 150, rewards = { { type = 'vehicle', model = 'jugular' } } },
    { id = 'revolter', category = 'vehicles', price = 1100, plateOption = 150, rewards = { { type = 'vehicle', model = 'revolter' } } },
    { id = 'toros', category = 'vehicles', price = 1300, plateOption = 150, rewards = { { type = 'vehicle', model = 'toros' } } },
    { id = 'shotaro', category = 'vehicles', price = 900, plateOption = 150, rewards = { { type = 'vehicle', model = 'shotaro' } } },
    { id = 'hakuchou2', category = 'vehicles', price = 800, plateOption = 150, rewards = { { type = 'vehicle', model = 'hakuchou2' } } },

    -- aéreos
    { id = 'supervolito', category = 'air', price = 3500, featured = true, rewards = { { type = 'vehicle', model = 'supervolito' } } },
    { id = 'frogger', category = 'air', price = 2800, image = 'img/heli.png', rewards = { { type = 'vehicle', model = 'frogger' } } },
    { id = 'swift', category = 'air', price = 3200, rewards = { { type = 'vehicle', model = 'swift' } } },
    { id = 'volatus', category = 'air', price = 4000, tag = 'LUXO', rewards = { { type = 'vehicle', model = 'volatus' } } },

    -- armas (itens do inventário)
    { id = 'w_pistol50', category = 'weapons', price = 350, rewards = { { type = 'item', name = 'weapon_pistol50', amount = 1 } } },
    { id = 'w_heavypistol', category = 'weapons', price = 300, rewards = { { type = 'item', name = 'weapon_heavypistol', amount = 1 } } },
    { id = 'w_smg', category = 'weapons', price = 600, rewards = { { type = 'item', name = 'weapon_smg', amount = 1 } } },
    { id = 'w_carbine', category = 'weapons', price = 900, rewards = { { type = 'item', name = 'weapon_carbinerifle', amount = 1 } } },
    { id = 'w_ak', category = 'weapons', price = 950, tag = 'HOT', rewards = { { type = 'item', name = 'weapon_assaultrifle', amount = 1 } } },
    { id = 'w_special', category = 'weapons', price = 1000, rewards = { { type = 'item', name = 'weapon_specialcarbine', amount = 1 } } },
    { id = 'w_pump', category = 'weapons', price = 500, rewards = { { type = 'item', name = 'weapon_pumpshotgun', amount = 1 } } },
    { id = 'w_sniper', category = 'weapons', price = 1200, rewards = { { type = 'item', name = 'weapon_sniperrifle', amount = 1 } } },

    -- dinheiro (no banco)
    { id = 'money_100k', category = 'money', label = '$100.000', price = 100, image = 'img/money.png', rewards = { { type = 'money', account = 'bank', amount = 100000 } } },
    { id = 'money_250k', category = 'money', label = '$250.000', price = 240, image = 'img/money.png', rewards = { { type = 'money', account = 'bank', amount = 250000 } } },
    { id = 'money_500k', category = 'money', label = '$500.000', price = 450, image = 'img/money.png', rewards = { { type = 'money', account = 'bank', amount = 500000 } } },
    { id = 'money_1m', category = 'money', label = '$1.000.000', price = 850, oldPrice = 1000, image = 'img/money.png', tag = 'POUPA 15%', rewards = { { type = 'money', account = 'bank', amount = 1000000 } } },

    -- chaves e skins das caixas (Base DRS PVP)
    { id = 'keys_5', category = 'cases', label = '5 Chaves', desc = 'Abre 5 caixas à escolha no F5 > Caixas.', price = 200, image = 'img/classica.png', rewards = { { type = 'keys', amount = 5 } } },
    { id = 'keys_15', category = 'cases', label = '15 Chaves', desc = 'Abre 15 caixas à escolha no F5 > Caixas.', price = 550, image = 'img/luxo.png', tag = '+1 GRÁTIS', rewards = { { type = 'keys', amount = 15 } } },
    { id = 'keys_40', category = 'cases', label = '40 Chaves', desc = 'Abre 40 caixas à escolha no F5 > Caixas.', price = 1300, oldPrice = 1600, image = 'img/caveira.png', featured = true, rewards = { { type = 'keys', amount = 40 } } },
    { id = 'skin_ak_caveira', category = 'cases', label = 'AK Mk II | Caveira', desc = 'A skin mais rara da Caixa Caveira, sem sorte nenhuma.', price = 900, tag = 'DOURADA', limit = 1,
      weapon = 'weapon_assaultrifle_mk2', rewards = { { type = 'skin', id = 'assaultrifle_mk2_camo_04' } } },
    { id = 'skin_pistol_luxe', category = 'cases', label = 'Pistola | Yusuf Amir', desc = 'Acabamento de luxo dourado para a pistola.', price = 400, limit = 1,
      weapon = 'weapon_pistol', rewards = { { type = 'skin', id = 'pistol_luxe' } } },

    -- pacotes (várias coisas juntas)
    { id = 'starter', category = 'bundles', label = 'Pacote Iniciante', desc = 'Para começar bem: carro, pistola, $150.000 e 5 chaves.', price = 900, oldPrice = 1350,
      image = 'img/hero.png', tag = 'SÓ 1 VEZ', limit = 1, featured = true,
      rewards = { { type = 'vehicle', model = 'jugular' }, { type = 'item', name = 'weapon_heavypistol', amount = 1 },
                  { type = 'money', account = 'bank', amount = 150000 }, { type = 'keys', amount = 5 } } },
    { id = 'gangster', category = 'bundles', label = 'Pacote Gangster', desc = 'AK, SMG, Revolter e 10 chaves.', price = 2300, oldPrice = 2950, image = 'img/redeem.png',
      rewards = { { type = 'vehicle', model = 'revolter' }, { type = 'item', name = 'weapon_assaultrifle', amount = 1 },
                  { type = 'item', name = 'weapon_smg', amount = 1 }, { type = 'keys', amount = 10 } } },

    -- pacotes VIP (a staff ativa: chega um pedido ao Discord)
    { id = 'vip_bronze', category = 'bundles', label = 'VIP Bronze', desc = 'A staff recebe o pedido no Discord e ativa o teu VIP.', price = 600, image = 'img/gen/vip_bronze.png', rewards = { { type = 'request', text = 'VIP BRONZE' } } },
    { id = 'vip_silver', category = 'bundles', label = 'VIP Prata', desc = 'A staff recebe o pedido no Discord e ativa o teu VIP.', price = 1000, image = 'img/gen/vip_silver.png', rewards = { { type = 'request', text = 'VIP PRATA' } } },
    { id = 'vip_gold', category = 'bundles', label = 'VIP Ouro', desc = 'A staff recebe o pedido no Discord e ativa o teu VIP.', price = 1800, image = 'img/gen/vip_gold.png', tag = 'POPULAR', featured = true, rewards = { { type = 'request', text = 'VIP OURO' } } },
    { id = 'vip_diamond', category = 'bundles', label = 'VIP Diamante', desc = 'A staff recebe o pedido no Discord e ativa o teu VIP.', price = 3000, image = 'img/gen/vip_diamond.png', rewards = { { type = 'request', text = 'VIP DIAMANTE' } } },

    -- casas do qb-houses (o nome é o da tabela houselocations; sai da loja quando tem dono)
    -- { id = 'house_vinewood', category = 'houses', label = 'Mansão Vinewood', price = 2500, image = 'img/gen/house.png', rewards = { { type = 'house', name = 'lake vinewood est1' } } },

    -- peds VIP (precisa do drs_pedvip; ou liga Config.PedCatalog para ler a tabela vip_ped_catalog)
    -- { id = 'ped_franklin', category = 'peds', label = 'Franklin', price = 800, image = 'img/gen/ped.png', limit = 1, rewards = { { type = 'ped', model = 'player_one' } } },

    -- extras
    { id = 'priority', category = 'extras', label = 'Prioridade na fila', desc = 'Entras primeiro na fila do servidor (fica guardado para sempre).', price = 500, image = 'img/gen/priority.png', limit = 1,
      rewards = { { type = 'priority', tier = 'VIP', power = 35 } } },
    { id = 'crypto_pack', category = 'money', label = 'Pack Crypto', desc = '10 de crypto na tua carteira.', price = 300, image = 'img/gen/crypto.png', rewards = { { type = 'money', account = 'crypto', amount = 10 } } },
    { id = 'phone_new', category = 'extras', label = 'Número novo', desc = 'Recebes um número de telemóvel novo ao calhas.', price = 100, image = 'img/gen/phone_new.png', rewards = { { type = 'phone', random = true } } },
    { id = 'plate', category = 'extras', label = 'Matrícula personalizada', desc = 'Muda a matrícula do carro onde estás (tem de ser teu). As chaves passam para a matrícula nova.',
      price = 150, image = 'img/plate.png', rewards = { { type = 'plate' } } },
    { id = 'phone', category = 'extras', label = 'Número de telemóvel', desc = 'Escolhe o teu número. Fica ativo depois de voltares a entrar no servidor.',
      price = 200, image = 'img/phone.png', rewards = { { type = 'phone' } } },
}

-- peds VIP lidos da base de dados (tabela vip_ped_catalog: name, price, enabled) e mostrados em "Personagens"
Config.PedCatalog = { enabled = false, category = 'peds', image = 'img/gen/ped.png' }

#Configuração do servidor (webhooks)

server_config.lua
-- Só no servidor (os jogadores não recebem este ficheiro)
ServerConfig = {
    Webhook = '',              -- webhook do Discord para as compras, códigos e coins dados pela staff ('' = desligado)
    WebhookName = 'DRS Donations',
    RequestWebhook = '',       -- pedidos dos produtos que a staff ativa à mão (VIPs, org...); '' = usa o Webhook
    MaxCoinsPerCommand = 100000, -- limite de segurança do /darcoins e do comando do Tebex
    RedeemTries = { perMinute = 5, blockAfter = 10, blockSeconds = 300 }, -- proteção contra quem tenta adivinhar códigos
}