Wróć do bloga
Guide9 min czytania

System napadów i skoków w FiveM: banki, sklepy i skarbce

Skrypt nagradzający napady i skoki w FiveM. Fleeca, Paleto, sklepy jubilerskie, policja i najlepsze skrypty napadów na bezkompromisowe serwery roleplay.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Przegląd architektury systemu napadów

Napady i skoki należą do najbardziej ekscytujących elementów rozgrywki na każdym serwerze FiveM roleplay. Łączą przestępców planujących skomplikowaną operację, policję reagującą z taktyczną precyzją, negocjatorów prowadzących napięte rozmowy o zakładników oraz cywilów uwięzionych w krzyżowym ogniu. Dobrze zaprojektowany system napadów wykracza daleko poza prosty komunikat interakcji dający graczom pieniądze. Wymaga warstwowej architektury zarządzającej lokalizacjami, sprawdzaniem wymagań wstępnych, kolejnymi fazami minigier, podziałem łupu, integracją z dyspozytornią policji, czasem odnowienia i zabezpieczeniami anti-exploit. System powinien składać się z trzech głównych warstw: warstwy konfiguracji definiującej każdą lokalizację napadu wraz z jej wymaganiami i nagrodami, serwerowej maszyny stanów śledzącej aktywne napady i egzekwującej wszystkie zasady oraz klienckiej warstwy prezentacji renderującej minigry, animacje i interfejsy NUI. Każda kluczowa decyzja musi zapadać po stronie serwera, ponieważ logikę po stronie klienta można banalnie łatwo zmanipulować.

Konfiguracja lokalizacji i poziomów napadów

Twój system napadów powinien obsługiwać wiele typów lokalizacji o rosnącym poziomie trudności. Sklepy spożywcze stanowią poziom podstawowy, wymagający minimalnej obecności policji i podstawowych narzędzi. Oddziały banku Fleeca znajdują się na średnim poziomie z umiarkowanymi zabezpieczeniami i wyższymi wymaganiami dotyczącymi policji. Skarbiec Pacific Standard to szczytowy napad, wymagający pełnej ekipy, zaawansowanego sprzętu i znacznej obecności służb mundurowych online. Każda lokalizacja wymaga wpisu w konfiguracji, który określa jej współrzędne, wymagane przedmioty, minimalną liczbę policjantów, czas odnowienia, zakres nagrody oraz sekwencję zabezpieczeń, które należy pokonać:

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

Każdy poziom powinien skalować się nie tylko pod względem nagrody, ale także złożoności i ryzyka. Napady na sklepy to szybkie akcje typu smash-and-grab trwające poniżej dwóch minut, podczas gdy napad na Pacific Standard to wieloetapowa operacja, której ukończenie może zająć piętnaście minut lub więcej, dając policji mnóstwo czasu na utworzenie kordonu.

Maszyna stanów po stronie serwera

Maszyna stanów napadu to serce systemu. Śledzi każdą lokalizację napadu przez stany jej cyklu życia: idle, in-progress, cooldown i locked. Gdy gracz rozpoczyna napad, serwer weryfikuje wszystkie wymagania wstępne przed zmianą stanu lokalizacji z idle na in-progress. W stanie in-progress serwer monitoruje, na której warstwie zabezpieczeń znajdują się obecnie przestępcy, którzy gracze biorą udział i ile czasu upłynęło. Jeśli wszyscy gracze opuszczą obszar lub zostaną aresztowani, napad automatycznie kończy się niepowodzeniem i przechodzi w skrócony stan cooldown. Po ukończeniu lokalizacja przechodzi w stan cooldown na skonfigurowany czas, zanim stanie się ponownie dostępna:

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)

Maszyna stanów powinna również radzić sobie ze skrajnymi przypadkami, takimi jak restart serwera podczas trwającego napadu. Po uruchomieniu serwera zresetuj wszystkie stany napadów do idle i wyczyść aktywnych uczestników. Zapisuj znaczniki czasu cooldownu w trwałej tabeli, aby przetrwały restarty.

Integracja minigier

Minigry przekształcają napady z biernego czekania w aktywne wyzwanie zręcznościowe. Każdy typ warstwy zabezpieczeń powinien odpowiadać innemu stylowi minigry, który testuje różne umiejętności gracza. Minigry hakerskie mogą wykorzystywać łamigłówki pamięciowe, wyzwania łączenia obwodów lub szybkie sekwencje liczb. Podkładanie termitu wymaga precyzyjnego wyczucia czasu, aby rozmieścić ładunki, zanim zgasną. Mechanika wiercenia sprawdza stabilną kontrolę myszy, gdy gracz prowadzi wiertło przez mechanizm zamka bez przegrzania. Kluczową zasadą projektowania jest to, że trudniejsze poziomy powinny używać bardziej złożonych wariantów tych minigier z węższymi oknami czasowymi, większą liczbą elementów do śledzenia lub większą prędkością:

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

Nieudane próby w minigrach powinny mieć konsekwencje. Nieudany hack może wywołać dodatkowe alarmy, skrócić pozostały czas lub zablokować gracza na krótki okres karny. W przypadku napadów wysokiego poziomu niepowodzenie ostatniego hackowania może całkowicie zablokować skarbiec, zmuszając ekipę do porzucenia napadu z jedynie częściowym łupem. Te stany niepowodzenia dodają napięcia i sprawiają, że udane ukończenie daje prawdziwą satysfakcję.

Integracja z dispatch i reagowaniem policji

System napadów jest kompletny tylko w połowie bez integracji z policją. Gdy rozpoczyna się napad, system dispatch powinien wysyłać stopniowane alerty w oparciu o poziom napadu. Napad na sklep spożywczy generuje proste powiadomienie o cichym alarmie z lokalizacją sklepu. Oddział Fleeca uruchamia alarm bankowy z nazwą placówki i sugerowanym poziomem reakcji. Napad na Pacific Standard aktywuje pełny komunikat alarmowy zalecający reakcję wszystkich dostępnych jednostek. Zapewnij policji aktualizacje w czasie rzeczywistym w miarę postępów napadu: gdy przestępcy przełamią kolejną warstwę zabezpieczeń, gdy wewnątrz budynku padną strzały lub gdy przestępcy podejmą próbę ucieczki. Te aktualizacje pomagają policji koordynować reakcję i tworzą dynamiczne scenariusze, w których funkcjonariusze mogą zdecydować, czy szturmować natychmiast, czy utworzyć kordon i negocjować. Zaimplementuj mechanikę zakładników, w której przestępcy mogą brać kasjerów NPC jako zakładników, dając pole do popisu negocjatorom policyjnym i zapewniając przestępcom kartę przetargową do opóźnienia reakcji policji. System zakładników powinien śledzić liczbę przetrzymywanych osób, to, czy ktokolwiek odniósł obrażenia, oraz dostarczać wskazówki negocjacyjne podczas rozmów telefonicznych między przestępcami a interweniującymi funkcjonariuszami.

Dystrybucja łupów i pranie brudnych pieniędzy

To, co gracze otrzymują z napadu, ma tak samo duże znaczenie jak sam napad. Zamiast wpłacać czystą gotówkę bezpośrednio na konta bankowe, nagradzaj przestępców oznaczonymi banknotami lub skradzionymi przedmiotami, które wymagają dodatkowych kroków, aby zamienić je na użyteczną walutę. Taki projekt wydłuża pętlę rozgrywki przestępczej poza sam napad i tworzy powiązane ze sobą systemy. Oznaczone banknoty można prać w określonych miejscach na mapie za procentową prowizję, co zmusza do kalkulacji ryzyka i zysku przy wyborze czasu oraz miejsca prania. Skradzioną biżuterię lub elektronikę trzeba sprzedać paserom NPC, którzy oferują zmienne stawki w zależności od rzadkości przedmiotu i aktualnych warunków rynkowych. Wdróż system toreb na łup, w którym fizyczny łup musi być noszony przez graczy, co ogranicza ich prędkość poruszania się i uniemożliwia użycie broni, dodając elementy taktyczne do fazy ucieczki:

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 i zarządzanie cooldownami

Systemy napadów przyciągają osoby szukające exploitów, ponieważ generują bogactwo. Wdróż wiele warstw ochrony przed powszechnymi nadużyciami. Serwerowe sprawdzanie odległości gwarantuje, że gracze są fizycznie obecni w miejscu napadu na każdym etapie. Ograniczanie częstotliwości zapytań zapobiega spamowaniu zdarzeniami rozpoczęcia napadu. Śledzenie uczestników zapewnia, że tylko zarejestrowani gracze mogą pokonywać kolejne zabezpieczenia lub odbierać łup. Globalne czasy odnowienia zapobiegają spamowaniu napadami na całym serwerze, ograniczając liczbę jednocześnie aktywnych akcji. Czasy odnowienia dla pojedynczego gracza uniemożliwiają jednej osobie dokonywanie serii napadów raz za razem. Zapisuj każdą próbę napadu wraz ze znacznikami czasu, identyfikatorami uczestników, statusem sukcesu lub porażki oraz przyznaną nagrodą, aby administratorzy mogli badać podejrzane schematy. Monitoruj niemożliwe czasy ukończenia, gdy gracz pokonuje wszystkie zabezpieczenia szybciej, niż jest to fizycznie możliwe. Wdróż system reputacji, w którym nadmierna aktywność przestępcza przyciąga większą uwagę policji i surowsze konsekwencje, tworząc naturalny bufor zachęcający graczy do rozkładania akcji przestępczych w czasie.

Drogi ucieczki i dynamiczne wydarzenia

Faza ucieczki to moment, w którym napady stają się naprawdę nieprzewidywalne. Po zabezpieczeniu łupu przestępcy muszą uciec z obszaru, niosąc ciężkie torby ograniczające ich ruchy. Zaprojektuj system tak, aby obsługiwał wiele metod ucieczki: pojazdy lądowe zaparkowane na zewnątrz, łodzie w pobliskich dokach w przypadku banków nad wodą lub ewakuację helikopterem z dachów przy napadach wyższego poziomu. Dodaj dynamiczne zdarzenia w fazie ucieczki, takie jak kolczatki rozkładane przez policję, komunikaty radiowe ujawniające opis pojazdu podejrzanych oraz losowe blokady drogowe na kluczowych skrzyżowaniach. Serwer powinien śledzić, czy przestępcy pomyślnie opuścili strefę napadu, zdefiniowaną jako konfigurowalny promień wokół banku, i dopiero wtedy oznaczać napad jako w pełni zakończony. Jeśli wszyscy uczestnicy zginą lub zostaną aresztowani przed opuszczeniem strefy, łup powinien być możliwy do odzyskania przez policję jako dowód. Tworzy to znaczącą rozgrywkę po napadzie dla obu stron i sprawia, że ucieczka jest równie wymagająca oraz satysfakcjonująca, co sam skok.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.