local activeRobberies = {} local prepSessions = {} local globalCooldownUntil = 0 local function nearLocation(source, coords, maximum) local ped = GetPlayerPed(source) if not ped or ped <= 0 then return false end local playerCoords = GetEntityCoords(ped) return #(playerCoords - vector3(coords.x, coords.y, coords.z)) <= maximum end local function findPrep(key) for _, entry in ipairs(DevXCrimeConfig.PrepLocations) do if entry.key == key then return entry end end return nil end local function getProgress(characterId) local row = MySQL.single.await([[ SELECT drill_done AS drillDone, card_done AS cardDone, vehicle_done AS vehicleDone FROM devx_heist_progress WHERE character_id = ? LIMIT 1 ]], { characterId }) if not row then MySQL.insert.await('INSERT IGNORE INTO devx_heist_progress (character_id) VALUES (?)', { characterId }) row = { drillDone = 0, cardDone = 0, vehicleDone = 0 } end return { drill = tonumber(row.drillDone) == 1, card = tonumber(row.cardDone) == 1, vehicle = tonumber(row.vehicleDone) == 1 } end local function leavePrep(source) prepSessions[source] = nil SetPlayerRoutingBucket(source, 0) end MySQL.ready(function() MySQL.query.await([[ CREATE TABLE IF NOT EXISTS devx_heist_progress ( character_id BIGINT UNSIGNED NOT NULL, drill_done TINYINT(1) NOT NULL DEFAULT 0, card_done TINYINT(1) NOT NULL DEFAULT 0, vehicle_done TINYINT(1) NOT NULL DEFAULT 0, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (character_id), CONSTRAINT fk_devx_heist_character FOREIGN KEY (character_id) REFERENCES devx_characters (id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ]]) end) DevXRegisterCallback('crime:status', function(source) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end return { ok = true, progress = getProgress(player.characterId), cooldownSeconds = math.max(0, globalCooldownUntil - os.time()), active = activeRobberies[source] ~= nil } end) DevXRegisterCallback('crime:enterPrep', function(source, payload) local player = exports.devx_core:GetPlayer(source) local prep = findPrep(tostring(payload.key or '')) if not player or not prep then return { ok = false, error = 'Ungültige Vorbereitungsmission.' } end if not nearLocation(source, prep.exterior, 6.0) then return { ok = false, error = 'Du bist nicht am Lagereingang.' } end local bucket = DevXCrimeConfig.MissionBucketBase + tonumber(source) prepSessions[source] = { key = prep.key, exterior = prep.exterior, bucket = bucket } SetRoutingBucketPopulationEnabled(bucket, false) SetRoutingBucketEntityLockdownMode(bucket, 'relaxed') SetPlayerRoutingBucket(source, bucket) return { ok = true, interior = DevXCrimeConfig.PrepInterior, prep = prep } end) DevXRegisterCallback('crime:leavePrep', function(source) local session = prepSessions[source] if not session then return { ok = false, error = 'Keine Vorbereitungsmission aktiv.' } end local exterior = session.exterior leavePrep(source) return { ok = true, exterior = exterior } end) DevXRegisterCallback('crime:completePrep', function(source, payload) local player = exports.devx_core:GetPlayer(source) local key = tostring(payload.key or '') local prep = findPrep(key) local session = prepSessions[source] if not player or not prep or not session or session.key ~= key then return { ok = false, error = 'Keine passende Vorbereitungsmission aktiv.' } end if not nearLocation(source, prep.objective, 5.0) then return { ok = false, error = 'Du bist nicht am Missionsziel.' } end local column = ({ drill = 'drill_done', card = 'card_done', vehicle = 'vehicle_done' })[key] MySQL.query.await('INSERT IGNORE INTO devx_heist_progress (character_id) VALUES (?)', { player.characterId }) MySQL.update.await(('UPDATE devx_heist_progress SET %s = 1 WHERE character_id = ?'):format(column), { player.characterId }) local exterior = session.exterior leavePrep(source) return { ok = true, progress = getProgress(player.characterId), exterior = exterior } end) DevXRegisterCallback('crime:startRobbery', function(source) local player = exports.devx_core:GetPlayer(source) if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end if not nearLocation(source, DevXCrimeConfig.Bank, 6.0) then return { ok = false, error = 'Du bist nicht im Tresorbereich.' } end if activeRobberies[source] then return { ok = false, error = 'Der Raub läuft bereits.' } end if os.time() < globalCooldownUntil then return { ok = false, error = ('Die Bank ist noch %s Minuten gesperrt.'):format(math.ceil((globalCooldownUntil - os.time()) / 60)) } end local progress = getProgress(player.characterId) if not progress.drill or not progress.card or not progress.vehicle then return { ok = false, error = 'Schließe zuerst alle drei Vorbereitungsmissionen ab.' } end local duration = DevXCrimeConfig.RobberyDurationSeconds activeRobberies[source] = { characterId = player.characterId, completeAt = os.time() + duration } globalCooldownUntil = os.time() + DevXCrimeConfig.GlobalCooldownSeconds return { ok = true, duration = duration } end) DevXRegisterCallback('crime:finishRobbery', function(source) local robbery = activeRobberies[source] local player = exports.devx_core:GetPlayer(source) if not robbery or not player or robbery.characterId ~= player.characterId then return { ok = false, error = 'Kein aktiver Bankraub.' } end if os.time() < robbery.completeAt then return { ok = false, error = 'Der Tresor ist noch nicht geöffnet.' } end if not nearLocation(source, DevXCrimeConfig.Bank, 12.0) then activeRobberies[source] = nil return { ok = false, error = 'Du hast den Bankraubbereich verlassen.' } end local reward = math.random(DevXCrimeConfig.MinimumReward, DevXCrimeConfig.MaximumReward) exports.devx_core:AddCash(source, reward, 'Fleeca-Bankraub') exports.devx_core:AddMetadataNumber(source, 'rpXp', 75) MySQL.update.await([[ UPDATE devx_heist_progress SET drill_done = 0, card_done = 0, vehicle_done = 0 WHERE character_id = ? ]], { player.characterId }) activeRobberies[source] = nil return { ok = true, reward = reward, xp = 75 } end) AddEventHandler('playerDropped', function() activeRobberies[source] = nil leavePrep(source) end) AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end for _, playerId in ipairs(GetPlayers()) do SetPlayerRoutingBucket(tonumber(playerId), 0) end end) RegisterCommand('resetheist', function(source) if source ~= 0 and not IsPlayerAceAllowed(source, 'devx.admin') then return end globalCooldownUntil = 0 activeRobberies = {} end, false)