Description
A working ESX bank vault heist: the player stands at the vault and presses E to run three sequential stages (breach panel, drill lock, crack safe), each with a progress bar, before the vault opens and pays out. This is the flagship multi-stage heist loop a roleplay server sells as endgame content.
Prompt Template
You are writing a FiveM resource for es_extended (stable, exports getSharedObject).
Produce TWO files (client.lua + server.lua) for a multi-stage bank vault heist.
Framework: ESX via exports['es_extended']:getSharedObject().
Client:
- A vault at [VAULT_COORDS] with an ordered Stages table [breach_panel, drill_lock,
crack_safe]. Track currentStage locally for RENDERING only.
- When the player is within 2.5m and presses E (control 38), run the next stage:
RequestAnimDict('anim@heists@fleeca_bank@drilling') + a `while not HasAnimDictLoaded`
loop, TaskPlayAnim the drill, run a ~10s progress loop sending SendNUIMessage
drillProgress, then ClearPedTasks and TriggerServerEvent('bankheist:completeStage', index).
- RegisterNetEvent('bankheist:stageAck') to advance the rendered stage / show vaultOpen.
Server:
- Track the real stage per player in a table keyed by source. Reject any
completeStage whose index != expected (DropPlayer on desync).
- Only on the final stage, call xPlayer.addMoney([LOOT_REWARD]).
- Clean the table on playerDropped.
Use PlayerPedId(), never GetPlayerPed(-1). The SERVER owns stage order and payout.
Return only Lua, separated by "-- ===== client.lua =====" and "-- ===== server.lua =====".
Expected Output
The reference Lua lives at content/expected-outputs/heists/01-bank-vault-drill-loot.lua. It implements the proximity drill loop with NUI progress, a server-side per-player stage counter that rejects out-of-order completions, and a one-time payout on the final stage. The fxmanifest splits it into a client_script (drill + NUI) and a server_script (stage validation + money).
-- Resource: bank-vault-heist (ESX)
-- Multi-stage vault drill + loot. client.lua drives the drill stages and props;
-- server.lua validates stage order and pays out loot.
-- ===== client.lua =====
local ESX = exports['es_extended']:getSharedObject()
local VaultCoords = vector3(253.4, 226.1, 101.8)
local Stages = { 'breach_panel', 'drill_lock', 'crack_safe' }
local currentStage = 0
local drilling = false
local function playDrillAnim()
local dict = 'anim@heists@fleeca_bank@drilling'
RequestAnimDict(dict)
while not HasAnimDictLoaded(dict) do Wait(0) end
TaskPlayAnim(PlayerPedId(), dict, 'drill_straight_idle', 8.0, -8.0, -1, 1, 0, false, false, false)
end
local function runStage(index)
if drilling then return end
drilling = true
playDrillAnim()
local duration = 10000
local started = GetGameTimer()
while GetGameTimer() - started < duration do
local pct = math.floor(((GetGameTimer() - started) / duration) * 100)
SendNUIMessage({ action = 'drillProgress', stage = Stages[index], pct = pct })
Wait(250)
end
ClearPedTasks(PlayerPedId())
drilling = false
TriggerServerEvent('bankheist:completeStage', index)
end
RegisterNetEvent('bankheist:stageAck')
AddEventHandler('bankheist:stageAck', function(nextIndex)
currentStage = nextIndex
if nextIndex > #Stages then
SendNUIMessage({ action = 'vaultOpen' })
end
end)
CreateThread(function()
while true do
local sleep = 1000
local ped = PlayerPedId()
local dist = #(GetEntityCoords(ped) - VaultCoords)
if dist < 2.5 and currentStage < #Stages then
sleep = 0
if IsControlJustReleased(0, 38) then
runStage(currentStage + 1)
end
end
Wait(sleep)
end
end)
-- ===== server.lua =====
local ESX = exports['es_extended']:getSharedObject()
local heistStage = {}
local LootReward = 25000
RegisterNetEvent('bankheist:completeStage')
AddEventHandler('bankheist:completeStage', function(claimedIndex)
local src = source
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer then return end
local expected = (heistStage[src] or 0) + 1
if claimedIndex ~= expected then
DropPlayer(src, 'Heist stage desync')
return
end
heistStage[src] = expected
if expected >= 3 then
xPlayer.addMoney(LootReward)
heistStage[src] = 0
end
TriggerClientEvent('bankheist:stageAck', src, expected + 1)
end)
AddEventHandler('playerDropped', function()
heistStage[source] = nil
end)
Known Failure Modes
- Client-trusted stages — Claude tracks the stage and pays loot client-side. Keep the authoritative counter server-side and reject any
claimedIndex ~= expected. - Anim before load —
TaskPlayAnimno-ops if the dict is not loaded; alwaysRequestAnimDict+while not HasAnimDictLoaded(dict) do Wait(0) end. - One-sided net event — defining the event on only one side breaks the round-trip; both client and server use
RegisterNetEvent+AddEventHandler. - No desync guard — without a stage-order check a packet can jump straight to payout; drop or ignore mismatched indices.
Integration Notes
- Split the banners into
client.luaandserver.lua; list both infxmanifest.lua(client_script 'client.lua',server_script 'server.lua'). - Requires
es_extendedstarted first. Pair theSendNUIMessagecalls with a simplenui/index.htmlprogress bar, or swap them for an ESX progressbar export. - Test on a dev server: stand at the vault, run all three stages in order, confirm money lands only after stage 3 and that replaying a stage out of order is rejected.
Profit Potential
$450–$6500/mo on Tebex (expected ~$1700). [INFERRED] priced inside the $50-389 FiveM script band against a hot endgame-heist niche; corpus median seller $11.85K/mo (signal-scraper tebex_snapshot n=100), scaled up for the flagship multi-stage resource.
Trend Signal
🔥 hot — [INFERRED] multi-stage heists are flagship endgame content servers pay up for.
Sales Angle
Position as the flagship endgame heist every serious RP server needs — a server-authoritative multi-stage bank vault that can't be packet-skipped to the payout. Recommended Tebex price $349.
Difficulty & Ship Time
intermediate · ships in 4-6h.