Guide 2026-05-20

FiveM Robbery & Heist System Development

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

Robbery System Architecture Overview

Robberies and heists are among the most thrilling gameplay loops on any FiveM roleplay server. They bring together criminals planning an intricate operation, police responding with tactical precision, hostage negotiators managing tense standoffs, and civilians caught in the crossfire. A well-designed robbery system goes far beyond a simple interaction prompt that hands players money. It requires a layered architecture that handles location management, prerequisite checks, progressive minigame phases, loot distribution, police dispatch integration, cooldown management, and anti-exploit protections. The system should be built with three core layers: a configuration layer that defines every robbery location with its requirements and rewards, a server-side state machine that tracks active robberies and enforces all rules, and a client-side presentation layer that renders minigames, animations, and NUI interfaces. Every critical decision must happen server-side because client-side logic is trivially exploitable.

Configuring Robbery Locations and Tiers

Your robbery system should support multiple location types with escalating difficulty tiers. Convenience stores represent the entry level, requiring minimal police presence and basic tools. Fleeca bank branches sit in the middle tier with moderate security layers and higher police requirements. The Pacific Standard vault serves as the pinnacle heist, demanding a full crew, advanced equipment, and significant law enforcement presence online. Each location needs a configuration entry that defines its coordinates, required items, minimum police count, cooldown duration, reward range, and the sequence of security layers that must be defeated:

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},
        },
    },
}

Each tier should scale not only in reward but also in complexity and risk. Store robberies are quick smash-and-grab affairs lasting under two minutes, while the Pacific Standard heist is a multi-phase operation that can take fifteen minutes or more to complete, giving police ample time to establish a perimeter.

Server-Side State Machine

The robbery state machine is the heart of the system. It tracks each robbery location through its lifecycle states: idle, in-progress, cooldown, and locked. When a player initiates a robbery, the server validates all prerequisites before transitioning the location from idle to in-progress. During the in-progress state, the server tracks which security layer the criminals are currently on, which players are participating, and how much time has elapsed. If all players leave the area or are arrested, the robbery automatically fails and enters a shortened cooldown. When completed, the location enters cooldown state for the configured duration before becoming available again:

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)

The state machine should also handle edge cases like server restarts during active robberies. When the server starts, reset all robbery states to idle and clear any active participants. Store cooldown timestamps in a persistent table so they survive restarts.

Minigame Integration

Minigames transform robberies from a passive waiting experience into an active skill challenge. Each security layer type should map to a different minigame style that tests different player abilities. Hacking minigames can use memory-match puzzles, circuit-connecting challenges, or rapid number sequences. Thermite placement requires precise timing to place charges before they fizzle out. Drilling mechanics test steady mouse control as the player navigates a drill bit through a lock mechanism without overheating. The key design principle is that harder tiers should use more complex variants of these minigames with tighter timing windows, more elements to track, or faster speeds:

-- 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)

Failed minigame attempts should have consequences. A failed hack might trigger additional alarms, reduce the remaining time, or lock out the player for a brief penalty period. For high-tier heists, failing the final hack could lock down the vault entirely, forcing the crew to abandon the robbery with only partial loot. These failure states add tension and make successful completions feel genuinely rewarding.

Police Dispatch and Response Integration

A robbery system is only half complete without police integration. When a robbery starts, the dispatch system should send graded alerts based on the robbery tier. A convenience store robbery generates a simple silent alarm notification with the store location. A Fleeca branch triggers a bank alarm with the branch name and a suggested response level. The Pacific Standard heist activates a full emergency broadcast recommending all available units respond. Provide police with real-time updates as the robbery progresses: when criminals breach a new security layer, when shots are fired inside the building, or when criminals attempt to flee. These updates help police coordinate their response and create dynamic scenarios where officers can decide whether to breach immediately or set up a perimeter and negotiate. Implement a hostage mechanic where criminals can take NPC bank tellers hostage, giving police negotiators a role and providing criminals with leverage to delay the police response. The hostage system should track how many hostages are held, whether any have been harmed, and provide negotiation prompts through phone calls between the criminals and the responding officers.

Loot Distribution and Money Laundering

What players receive from a robbery matters as much as the robbery itself. Instead of depositing clean cash directly into bank accounts, reward criminals with marked bills or stolen goods that require additional steps to convert into usable currency. This design extends the criminal gameplay loop beyond the heist and creates interconnected systems. Marked bills can be laundered at specific locations around the map for a percentage fee, creating risk-reward decisions about when and where to launder. Stolen jewelry or electronics need to be fenced through NPC dealers who offer variable rates depending on the item rarity and current market conditions. Implement a loot bag system where the physical loot must be carried by players, limiting their movement speed and preventing weapon use, which adds tactical considerations to the escape phase:

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)

Anti-Exploit and Cooldown Management

Robbery systems attract exploiters because they generate wealth. Implement multiple layers of protection against common exploits. Server-side distance checks ensure players are physically present at the robbery location throughout every phase. Rate limiting prevents players from spamming robbery start events. Participant tracking ensures only registered participants can progress through security layers or receive loot. Global cooldowns prevent server-wide robbery spam by limiting how many robberies can be active simultaneously. Per-player cooldowns prevent a single player from chain-robbing multiple locations back to back. Log every robbery attempt with timestamps, participant identifiers, success or failure status, and the reward distributed so administrators can investigate suspicious patterns. Monitor for impossible completion times where a player finishes all security layers faster than should be physically possible. Implement a reputation system where excessive robbery activity draws increased police attention and harsher consequences, creating a natural throttle that encourages players to space out their criminal activities.

Escape Routes and Dynamic Events

The escape phase is where robberies become truly unpredictable. Once the loot is secured, criminals must escape the area while carrying heavy duffel bags that restrict their movement. Design the system to support multiple escape methods: ground vehicles parked outside, boats at nearby docks for waterfront banks, or helicopter extractions from rooftops for high-tier heists. Add dynamic events during the escape phase such as tire spike strips that police can deploy, radio chatter updates that reveal the suspect vehicle description, and random roadblocks at key intersections. The server should track whether the criminals successfully leave the robbery zone, defined as a configurable radius around the bank, and only then mark the robbery as fully complete. If all participants are killed or arrested before clearing the zone, the loot should be recoverable by police as evidence. This creates meaningful post-robbery gameplay for both sides and ensures that escaping is just as challenging and rewarding as the heist itself.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.