Visão geral da arquitetura do sistema de roubo
Roubos e assaltos estão entre os ciclos de jogo mais emocionantes em qualquer servidor de RPG FiveM. Eles reúnem criminosos que planejam uma operação complexa, policiais que respondem com precisão tática, negociadores de reféns que gerenciam impasses tensos e civis apanhados no fogo cruzado. Um sistema de roubo bem projetado vai muito além de um simples prompt de interação que entrega dinheiro aos jogadores. Requer uma arquitetura em camadas que lide com gerenciamento de localização, verificações de pré-requisitos, fases progressivas de minijogos, distribuição de saques, integração de despacho policial, gerenciamento de resfriamento e proteções anti-exploração. O sistema deve ser construído com três camadas principais: uma camada de configuração que define cada local de roubo com seus requisitos e recompensas, uma máquina de estado do lado do servidor que rastreia roubos ativos e aplica todas as regras, e uma camada de apresentação do lado do cliente que renderiza minijogos, animações e interfaces NUI. Cada decisão crítica deve acontecer no lado do servidor porque a lógica do lado do cliente é trivialmente explorável.
Configurando locais e níveis de roubo
Seu sistema de roubo deve suportar vários tipos de locais com níveis de dificuldade crescentes. As lojas de conveniência representam o nível de entrada, exigindo presença policial mínima e ferramentas básicas. As agências bancárias Fleeca ficam na camada intermediária, com camadas de segurança moderadas e requisitos policiais mais elevados. O cofre Pacific Standard serve como o assalto máximo, exigindo uma tripulação completa, equipamento avançado e presença significativa de autoridades policiais online. Cada local precisa de uma entrada de configuração que defina suas coordenadas, itens necessários, contagem mínima de policiais, duração do tempo de espera, intervalo de recompensas e a sequência de camadas de segurança que devem ser derrotadas:
Config.Robberies = {
['store_1'] = {
label = 'LTD Gasoline - Davis',
type = 'store',
coords = vector3(-47.02, -1757.23, 29.42),
register = vector3(-43.43, -1748.60, 29.42),
tier = 1,
cooldown = 1800, -- 30 minutes
minPolice = 2,
requiredItems = {'weapon_pistol'},
reward = {min = 2500, max = 6000, type = 'cash'},
securityLayers = {
{type = 'intimidate', time = 30},
{type = 'grab_cash', time = 15},
},
},
['fleeca_legion'] = {
label = 'Fleeca Bank - Legion Square',
type = 'bank',
coords = vector3(149.73, -1042.65, 29.37),
vault = vector3(144.87, -1044.16, 29.37),
tier = 2,
cooldown = 7200, -- 2 hours
minPolice = 4,
requiredItems = {'electronickit', 'thermite'},
reward = {min = 45000, max = 90000, type = 'markedbills'},
securityLayers = {
{type = 'hack', difficulty = 'medium', time = 25},
{type = 'thermite', time = 10},
{type = 'drill', time = 40},
},
},
['pacific_standard'] = {
label = 'Pacific Standard Bank',
type = 'vault',
coords = vector3(255.85, 225.60, 101.88),
vault = vector3(262.20, 222.10, 101.68),
tier = 3,
cooldown = 14400, -- 4 hours
minPolice = 6,
requiredItems = {'electronickit', 'thermite', 'advancedlaptop', 'duffelbag'},
reward = {min = 250000, max = 500000, type = 'markedbills'},
securityLayers = {
{type = 'hack', difficulty = 'hard', time = 20},
{type = 'hack', difficulty = 'hard', time = 20},
{type = 'thermite', time = 8},
{type = 'drill', time = 60},
{type = 'hack', difficulty = 'expert', time = 15},
},
},
}Cada nível deve ser dimensionado não apenas em recompensa, mas também em complexidade e risco. Os assaltos a lojas são ataques rápidos que duram menos de dois minutos, enquanto o assalto ao Pacific Standard é uma operação multifásica que pode levar quinze minutos ou mais para ser concluída, dando à polícia tempo suficiente para estabelecer um perímetro.
Máquina de estado do lado do servidor
A máquina estatal de roubo é o coração do sistema. Ele rastreia cada local de roubo através de seus estados de ciclo de vida: inativo, em andamento, em espera e bloqueado. Quando um jogador inicia um assalto, o servidor valida todos os pré-requisitos antes de fazer a transição do local de inativo para em andamento. Durante o estado em andamento, o servidor rastreia em qual camada de segurança os criminosos estão atualmente, quais jogadores estão participando e quanto tempo já passou. Se todos os jogadores saírem da área ou forem presos, o roubo falha automaticamente e entra em um tempo de espera reduzido. Quando concluído, o local entra no estado de espera pela duração configurada antes de ficar disponível novamente:
local RobberyState = {}
function InitializeRobbery(robberyId)
RobberyState[robberyId] = {
status = 'idle', -- idle | active | cooldown | locked
participants = {},
currentLayer = 0,
startedAt = 0,
cooldownUntil = 0,
}
end
RegisterNetEvent('robbery:server:startRobbery', function(robberyId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local config = Config.Robberies[robberyId]
local state = RobberyState[robberyId]
if not config or not state then return end
-- Validate state
if state.status ~= 'idle' then
TriggerClientEvent('QBCore:Notify', src, 'This location is not available', 'error')
return
end
-- Check cooldown
if os.time() < state.cooldownUntil then
TriggerClientEvent('QBCore:Notify', src, 'Location on cooldown', 'error')
return
end
-- Check minimum police online
local policeCount = GetOnlinePolicCount()
if policeCount < config.minPolice then
TriggerClientEvent('QBCore:Notify', src, 'Not enough police online', 'error')
return
end
-- Check required items
for _, item in ipairs(config.requiredItems) do
if not Player.Functions.GetItemByName(item) then
TriggerClientEvent('QBCore:Notify', src, 'Missing required equipment', 'error')
return
end
end
-- Start robbery
state.status = 'active'
state.participants = {src}
state.currentLayer = 1
state.startedAt = os.time()
-- Alert police dispatch
TriggerEvent('dispatch:server:notify', {
type = 'robbery',
coords = config.coords,
message = 'Silent alarm triggered at ' .. config.label,
tier = config.tier,
})
-- Send first security layer to client
TriggerClientEvent('robbery:client:startLayer', src, robberyId, config.securityLayers[1])
end)A máquina de estado também deve lidar com casos extremos, como reinicializações de servidores durante roubos ativos. Quando o servidor for iniciado, redefina todos os estados de roubo para inativos e limpe todos os participantes ativos. Armazene carimbos de data/hora de resfriamento em uma tabela persistente para que sobrevivam às reinicializações.
Integração de minijogos
Os minijogos transformam os roubos de uma experiência de espera passiva em um desafio de habilidade ativo. Cada tipo de camada de segurança deve ser mapeado para um estilo de minijogo diferente que testa diferentes habilidades dos jogadores. Os minijogos de hacking podem usar quebra-cabeças de memória, desafios de conexão de circuitos ou sequências numéricas rápidas. A colocação do Thermite requer um tempo preciso para colocar as cargas antes que elas acabem. A mecânica de perfuração testa o controle constante do mouse enquanto o jogador navega com uma broca através de um mecanismo de trava sem superaquecimento. O princípio chave do design é que níveis mais difíceis devem usar variantes mais complexas desses minijogos com janelas de tempo mais restritas, mais elementos para rastrear ou velocidades mais rápidas:
-- Client-side minigame dispatcher
function StartMinigame(layerType, difficulty, callback)
if layerType == 'hack' then
local config = HackDifficulty[difficulty]
exports['ps-ui']:Circle(function(success)
callback(success)
end, config.circles, config.speed)
elseif layerType == 'thermite' then
exports['ps-ui']:Thermite(function(success)
callback(success)
end, 10, 5, 3)
elseif layerType == 'drill' then
exports['drilling']:StartDrill(function(success)
callback(success)
end)
elseif layerType == 'intimidate' then
-- Store clerk intimidation: aim weapon at NPC
StartClerkIntimidation(function(success)
callback(success)
end)
end
end
RegisterNetEvent('robbery:client:startLayer', function(robberyId, layer)
local ped = PlayerPedId()
-- Play appropriate animation
if layer.type == 'hack' then
PlayHackingAnimation(ped)
elseif layer.type == 'drill' then
PlayDrillingAnimation(ped)
end
StartMinigame(layer.type, layer.difficulty, function(success)
ClearPedTasks(ped)
TriggerServerEvent('robbery:server:layerResult', robberyId, success)
end)
end)Tentativas fracassadas de minijogo devem ter consequências. Um hack falhado pode acionar alarmes adicionais, reduzir o tempo restante ou bloquear o jogador por um breve período de penalidade. Para assaltos de alto nível, falhar no hack final poderia bloquear totalmente o cofre, forçando a tripulação a abandonar o roubo com apenas saque parcial. Esses estados de fracasso aumentam a tensão e fazem com que as conclusões bem-sucedidas pareçam genuinamente gratificantes.
Integração de Despacho Policial e Resposta
Um sistema de roubo está apenas pela metade sem a integração policial. Quando um roubo começa, o sistema de despacho deve enviar alertas classificados com base no nível do roubo. Um assalto a uma loja de conveniência gera uma simples notificação de alarme silencioso com a localização da loja. Uma agência Fleeca aciona um alarme bancário com o nome da agência e um nível de resposta sugerido. O assalto ao Pacific Standard ativa uma transmissão de emergência completa recomendando que todas as unidades disponíveis respondam. Forneça à polícia atualizações em tempo real à medida que o roubo avança: quando os criminosos violam uma nova camada de segurança, quando tiros são disparados dentro do prédio ou quando os criminosos tentam fugir. Estas atualizações ajudam a polícia a coordenar a sua resposta e a criar cenários dinâmicos onde os agentes podem decidir se violam imediatamente ou estabelecem um perímetro e negociam. Implemente uma mecânica de reféns onde os criminosos possam fazer reféns os caixas de banco NPC, dando aos negociadores da polícia um papel e proporcionando aos criminosos uma vantagem para atrasar a resposta da polícia. O sistema de reféns deve monitorizar quantos reféns estão detidos, se algum deles foi ferido, e fornecer avisos de negociação através de chamadas telefónicas entre os criminosos e os agentes responsáveis pela resposta.
Distribuição de saques e lavagem de dinheiro
O que os jogadores recebem de um roubo é tão importante quanto o roubo em si. Em vez de depositar dinheiro limpo diretamente em contas bancárias, recompense os criminosos com notas marcadas ou bens roubados que exigem etapas adicionais para serem convertidos em moeda utilizável. Este design estende o ciclo de jogo criminoso além do roubo e cria sistemas interconectados. As notas marcadas podem ser lavadas em locais específicos do mapa por uma taxa percentual, criando decisões de risco-recompensa sobre quando e onde lavar. Joias ou eletrônicos roubados precisam ser protegidos por revendedores NPC que oferecem taxas variáveis dependendo da raridade do item e das condições atuais do mercado. Implemente um sistema de loot bag onde o loot físico deve ser carregado pelos jogadores, limitando sua velocidade de movimento e evitando o uso de armas, o que acrescenta considerações táticas à fase de fuga:
RegisterNetEvent('robbery:server:distributeLoot', function(robberyId)
local src = source
local config = Config.Robberies[robberyId]
local state = RobberyState[robberyId]
if not config or not state then return end
if state.status ~= 'active' then return end
local rewardAmount = math.random(config.reward.min, config.reward.max)
local participants = state.participants
local perPlayer = math.floor(rewardAmount / #participants)
for _, playerId in ipairs(participants) do
local Player = QBCore.Functions.GetPlayer(playerId)
if Player then
if config.reward.type == 'markedbills' then
local billStacks = math.ceil(perPlayer / 5000)
for i = 1, billStacks do
local amount = math.min(5000, perPlayer - ((i - 1) * 5000))
Player.Functions.AddItem('markedbills', 1, nil, {
worth = amount,
source = config.label,
})
end
elseif config.reward.type == 'cash' then
Player.Functions.AddMoney('cash', perPlayer, 'robbery-' .. robberyId)
end
TriggerClientEvent('inventory:client:ItemBox', playerId, QBCore.Shared.Items['markedbills'], 'add')
end
end
-- Set cooldown
state.status = 'cooldown'
state.cooldownUntil = os.time() + config.cooldown
state.participants = {}
-- Log the robbery for admin review
LogRobbery(robberyId, participants, rewardAmount)
end)Gerenciamento anti-exploração e resfriamento
Os sistemas de roubo atraem exploradores porque geram riqueza. Implemente diversas camadas de proteção contra explorações comuns. As verificações de distância no servidor garantem que os jogadores estejam fisicamente presentes no local do roubo durante todas as fases. A limitação de taxa evita que os jogadores enviem spam para eventos de início de roubo. O rastreamento dos participantes garante que apenas os participantes registrados possam progredir nas camadas de segurança ou receber itens. Os cooldowns globais evitam spam de roubo em todo o servidor, limitando quantos roubos podem estar ativos simultaneamente. Os cooldowns por jogador evitam que um único jogador roube vários locais consecutivos. Registre cada tentativa de roubo com carimbos de data/hora, identificadores de participantes, status de sucesso ou falha e a recompensa distribuída para que os administradores possam investigar padrões suspeitos. Monitore os tempos de conclusão impossíveis em que um jogador conclui todas as camadas de segurança mais rápido do que deveria ser fisicamente possível. Implemente um sistema de reputação onde atividades de roubo excessivo atraiam maior atenção da polícia e consequências mais severas, criando um acelerador natural que incentiva os jogadores a espaçar suas atividades criminosas.
Rotas de fuga e eventos dinâmicos
A fase de fuga é onde os roubos se tornam verdadeiramente imprevisíveis. Depois que o saque estiver seguro, os criminosos devem escapar da área carregando mochilas pesadas que restringem seus movimentos. Projete o sistema para suportar vários métodos de fuga: veículos terrestres estacionados do lado de fora, barcos em docas próximas para bancos à beira-mar ou extrações de helicópteros de telhados para assaltos de alto nível. Adicione eventos dinâmicos durante a fase de fuga, como tiras de pneus que a polícia pode implantar, atualizações de mensagens de rádio que revelam a descrição do veículo suspeito e bloqueios aleatórios em cruzamentos importantes. O servidor deve rastrear se os criminosos saíram com sucesso da zona de roubo, definida como um raio configurável ao redor do banco, e só então marcar o roubo como totalmente concluído. Se todos os participantes forem mortos ou presos antes de saírem da zona, o saque deverá ser recuperado pela polícia como prova. Isso cria uma jogabilidade pós-roubo significativa para ambos os lados e garante que a fuga seja tão desafiadora e gratificante quanto o próprio assalto.
