first commit
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
local progress = { drill = false, card = false, vehicle = false }
|
||||
local robberyRunning = false
|
||||
local activePrep = nil
|
||||
local objectiveProp = nil
|
||||
local plannerPeds = {}
|
||||
local warehousePeds = {}
|
||||
local missionProps = {}
|
||||
local missionVehicles = {}
|
||||
local cutsceneRunning = false
|
||||
|
||||
local function notify(message) exports.devx_core:Notify(message) end
|
||||
local function help(message)
|
||||
BeginTextCommandDisplayHelp('STRING')
|
||||
AddTextComponentSubstringPlayerName(message)
|
||||
EndTextCommandDisplayHelp(0, false, true, -1)
|
||||
end
|
||||
local function subtitle(message, duration)
|
||||
BeginTextCommandPrint('STRING')
|
||||
AddTextComponentSubstringPlayerName(message)
|
||||
EndTextCommandPrint(duration or 3500, true)
|
||||
end
|
||||
local function drawMarkerAt(coords, red, green, blue)
|
||||
DrawMarker(1, coords.x, coords.y, coords.z - 1.0, 0.0,0.0,0.0,0.0,0.0,0.0,
|
||||
1.2,1.2,0.35,red,green,blue,130,false,false,2,false,nil,nil,false)
|
||||
end
|
||||
local function loadModel(name)
|
||||
local hash = GetHashKey(name)
|
||||
RequestModel(hash)
|
||||
local deadline = GetGameTimer() + 7000
|
||||
while not HasModelLoaded(hash) and GetGameTimer() < deadline do Wait(0) end
|
||||
return HasModelLoaded(hash) and hash or nil
|
||||
end
|
||||
local function loadAnim(dict)
|
||||
RequestAnimDict(dict)
|
||||
local deadline = GetGameTimer() + 5000
|
||||
while not HasAnimDictLoaded(dict) and GetGameTimer() < deadline do Wait(0) end
|
||||
return HasAnimDictLoaded(dict)
|
||||
end
|
||||
local function teleport(coords)
|
||||
DoScreenFadeOut(250)
|
||||
local deadline = GetGameTimer() + 1500
|
||||
while not IsScreenFadedOut() and GetGameTimer() < deadline do Wait(0) end
|
||||
local ped = PlayerPedId()
|
||||
FreezeEntityPosition(ped, true)
|
||||
if coords.z < -10.0 then SetFocusPosAndVel(coords.x, coords.y, coords.z, 0.0, 0.0, 0.0) end
|
||||
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
|
||||
SetEntityCoordsNoOffset(ped, coords.x, coords.y, coords.z, false, false, false)
|
||||
SetEntityHeading(ped, coords.w or coords.heading or 0.0)
|
||||
local collisionDeadline = GetGameTimer() + 5000
|
||||
while not HasCollisionLoadedAroundEntity(ped) and GetGameTimer() < collisionDeadline do
|
||||
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
|
||||
Wait(0)
|
||||
end
|
||||
ClearPedTasksImmediately(ped)
|
||||
FreezeEntityPosition(ped, false)
|
||||
Wait(180)
|
||||
ClearFocus()
|
||||
DoScreenFadeIn(350)
|
||||
end
|
||||
|
||||
local function ensurePrepInterior()
|
||||
local interior = DevXCrimeConfig.PrepInterior
|
||||
local position = interior.loadAt or interior.spawn
|
||||
SetFocusPosAndVel(position.x, position.y, position.z, 0.0, 0.0, 0.0)
|
||||
RequestCollisionAtCoord(position.x, position.y, position.z)
|
||||
RequestAdditionalCollisionAtCoord(position.x, position.y, position.z)
|
||||
NewLoadSceneStartSphere(position.x, position.y, position.z, 110.0, 0)
|
||||
|
||||
local interiorId = GetInteriorAtCoords(position.x, position.y, position.z)
|
||||
if interiorId ~= 0 then
|
||||
PinInteriorInMemory(interiorId)
|
||||
RefreshInterior(interiorId)
|
||||
end
|
||||
|
||||
local deadline = GetGameTimer() + 12000
|
||||
local ready = false
|
||||
while GetGameTimer() < deadline do
|
||||
local sceneReady = IsNewLoadSceneLoaded()
|
||||
local interiorReady = interiorId == 0 or IsInteriorReady(interiorId)
|
||||
if sceneReady and interiorReady then
|
||||
ready = true
|
||||
break
|
||||
end
|
||||
RequestCollisionAtCoord(position.x, position.y, position.z)
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
NewLoadSceneStop()
|
||||
ClearFocus()
|
||||
if not ready then
|
||||
print(('[devx_crime] Sicheres Garagen-Interior nicht bereit. interior=%s'):format(interiorId))
|
||||
end
|
||||
return ready
|
||||
end
|
||||
|
||||
local function deletePeds(list)
|
||||
for _, ped in pairs(list) do
|
||||
if ped and DoesEntityExist(ped) then DeleteEntity(ped) end
|
||||
end
|
||||
for key in pairs(list) do list[key] = nil end
|
||||
end
|
||||
|
||||
|
||||
local function deleteProps(list)
|
||||
for _, object in pairs(list) do
|
||||
if object and DoesEntityExist(object) then DeleteEntity(object) end
|
||||
end
|
||||
for key in pairs(list) do list[key] = nil end
|
||||
end
|
||||
|
||||
local function deleteVehicles(list)
|
||||
for _, vehicle in pairs(list) do
|
||||
if vehicle and DoesEntityExist(vehicle) then DeleteEntity(vehicle) end
|
||||
end
|
||||
for key in pairs(list) do list[key] = nil end
|
||||
end
|
||||
|
||||
local function spawnMissionVehicles()
|
||||
deleteVehicles(missionVehicles)
|
||||
for index, entry in ipairs(DevXCrimeConfig.PrepInterior.vehicles or {}) do
|
||||
local model = loadModel(entry.model)
|
||||
if model then
|
||||
local c = entry.coords
|
||||
local vehicle = CreateVehicle(model, c.x, c.y, c.z, c.w or 0.0, false, false)
|
||||
SetVehicleOnGroundProperly(vehicle)
|
||||
SetVehicleDoorsLocked(vehicle, 2)
|
||||
SetEntityInvincible(vehicle, true)
|
||||
FreezeEntityPosition(vehicle, true)
|
||||
missionVehicles[index] = vehicle
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function spawnMissionSetDressing()
|
||||
deleteProps(missionProps)
|
||||
for index, entry in ipairs(DevXCrimeConfig.PrepInterior.setDressing or {}) do
|
||||
local model = loadModel(entry.model)
|
||||
if model then
|
||||
local c = entry.coords
|
||||
local object = CreateObject(model, c.x, c.y, c.z, false, false, false)
|
||||
SetEntityHeading(object, c.w or 0.0)
|
||||
FreezeEntityPosition(object, true)
|
||||
SetEntityCollision(object, true, true)
|
||||
missionProps[index] = object
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function spawnPlanners()
|
||||
deletePeds(plannerPeds)
|
||||
local model = loadModel(DevXCrimeConfig.PlannerModel)
|
||||
if not model then return end
|
||||
for _, prep in ipairs(DevXCrimeConfig.PrepLocations) do
|
||||
local c = prep.planner
|
||||
local ped = CreatePed(4, model, c.x, c.y, c.z - 1.0, c.w, false, false)
|
||||
SetEntityInvincible(ped, true)
|
||||
SetBlockingOfNonTemporaryEvents(ped, true)
|
||||
FreezeEntityPosition(ped, true)
|
||||
TaskStartScenarioInPlace(ped, 'WORLD_HUMAN_CLIPBOARD', 0, true)
|
||||
plannerPeds[prep.key] = ped
|
||||
end
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
|
||||
local function spawnWarehousePeds()
|
||||
deletePeds(warehousePeds)
|
||||
for index, worker in ipairs(DevXCrimeConfig.WarehouseWorkers or {}) do
|
||||
local model = loadModel(worker.model)
|
||||
if model then
|
||||
local c = worker.coords
|
||||
local ped = CreatePed(4, model, c.x, c.y, c.z, c.w, false, false)
|
||||
SetEntityInvincible(ped, true)
|
||||
SetBlockingOfNonTemporaryEvents(ped, true)
|
||||
FreezeEntityPosition(ped, true)
|
||||
TaskStartScenarioInPlace(ped, worker.scenario or 'WORLD_HUMAN_CLIPBOARD', 0, true)
|
||||
warehousePeds[index] = ped
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function removeObjectiveProp()
|
||||
if objectiveProp and DoesEntityExist(objectiveProp) then DeleteEntity(objectiveProp) end
|
||||
objectiveProp = nil
|
||||
end
|
||||
local function spawnObjectiveProp(prep)
|
||||
removeObjectiveProp()
|
||||
local model = loadModel(prep.prop)
|
||||
if not model then return end
|
||||
objectiveProp = CreateObject(model, prep.objective.x, prep.objective.y, prep.objective.z + 0.05, false, false, false)
|
||||
SetEntityHeading(objectiveProp, prep.objective.w or 0.0)
|
||||
FreezeEntityPosition(objectiveProp, true)
|
||||
SetModelAsNoLongerNeeded(model)
|
||||
end
|
||||
local function timedAction(label, seconds)
|
||||
local endTime = GetGameTimer() + seconds * 1000
|
||||
FreezeEntityPosition(PlayerPedId(), true)
|
||||
while GetGameTimer() < endTime do
|
||||
local left = math.ceil((endTime - GetGameTimer()) / 1000)
|
||||
help(('%s ~b~%ss'):format(label, left))
|
||||
DisableAllControlActions(0)
|
||||
EnableControlAction(0, 200, true)
|
||||
Wait(0)
|
||||
end
|
||||
FreezeEntityPosition(PlayerPedId(), false)
|
||||
end
|
||||
local function refreshStatus()
|
||||
DevXTriggerCallback('crime:status', {}, function(result)
|
||||
if result.ok then progress = result.progress or progress end
|
||||
end)
|
||||
end
|
||||
local function leavePrep()
|
||||
if not activePrep then return end
|
||||
DevXTriggerCallback('crime:leavePrep', {}, function(result)
|
||||
if result.ok then
|
||||
teleport(result.exterior)
|
||||
TriggerEvent('devx_ipl:client:restoreWorld', 'crime-leave')
|
||||
end
|
||||
end)
|
||||
activePrep = nil
|
||||
removeObjectiveProp()
|
||||
deletePeds(warehousePeds)
|
||||
deleteProps(missionProps)
|
||||
deleteVehicles(missionVehicles)
|
||||
end
|
||||
|
||||
local function runPlanningScene(prep, callback)
|
||||
if cutsceneRunning then return end
|
||||
cutsceneRunning = true
|
||||
local planner = plannerPeds[prep.key]
|
||||
if not planner or not DoesEntityExist(planner) then
|
||||
cutsceneRunning = false
|
||||
callback()
|
||||
return
|
||||
end
|
||||
|
||||
local player = PlayerPedId()
|
||||
FreezeEntityPosition(player, true)
|
||||
ClearPedTasksImmediately(planner)
|
||||
TaskTurnPedToFaceEntity(player, planner, 1500)
|
||||
TaskTurnPedToFaceEntity(planner, player, 1500)
|
||||
Wait(1000)
|
||||
|
||||
local playerCoords = GetEntityCoords(player)
|
||||
local plannerCoords = GetEntityCoords(planner)
|
||||
local midpoint = (playerCoords + plannerCoords) / 2.0
|
||||
local cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
|
||||
SetCamCoord(cam, midpoint.x + 2.4, midpoint.y + 2.4, midpoint.z + 1.45)
|
||||
PointCamAtCoord(cam, midpoint.x, midpoint.y, midpoint.z + 0.85)
|
||||
SetCamFov(cam, 43.0)
|
||||
SetCamActive(cam, true)
|
||||
RenderScriptCams(true, true, 600, true, true)
|
||||
|
||||
if loadAnim('gestures@m@standing@casual') then
|
||||
for _, line in ipairs(prep.dialogue or {}) do
|
||||
TaskPlayAnim(planner, 'gestures@m@standing@casual', 'gesture_hand_down', 2.0, -2.0, 2800, 49, 0.0, false, false, false)
|
||||
subtitle(('~b~%s:~s~ %s'):format(DevXCrimeConfig.PlannerName, line), 3300)
|
||||
Wait(3400)
|
||||
end
|
||||
else
|
||||
for _, line in ipairs(prep.dialogue or {}) do
|
||||
subtitle(('~b~%s:~s~ %s'):format(DevXCrimeConfig.PlannerName, line), 3300)
|
||||
Wait(3400)
|
||||
end
|
||||
end
|
||||
|
||||
RenderScriptCams(false, true, 550, true, true)
|
||||
DestroyCam(cam, false)
|
||||
FreezeEntityPosition(player, false)
|
||||
TaskStartScenarioInPlace(planner, 'WORLD_HUMAN_CLIPBOARD', 0, true)
|
||||
cutsceneRunning = false
|
||||
callback()
|
||||
end
|
||||
|
||||
local function beginPrep(prep)
|
||||
runPlanningScene(prep, function()
|
||||
DevXTriggerCallback('crime:enterPrep', { key = prep.key }, function(result)
|
||||
if not result.ok then notify(result.error); return end
|
||||
if not ensurePrepInterior() then
|
||||
notify('Das Missionslager konnte nicht geladen werden. Die Mission wurde sicher abgebrochen.')
|
||||
DevXTriggerCallback('crime:leavePrep', {}, function() end)
|
||||
return
|
||||
end
|
||||
activePrep = result.prep
|
||||
teleport(result.interior.spawn)
|
||||
spawnMissionSetDressing()
|
||||
spawnMissionVehicles()
|
||||
spawnObjectiveProp(activePrep)
|
||||
spawnWarehousePeds()
|
||||
subtitle(('~b~MISSION:~s~ %s'):format(activePrep.objectiveLabel), 4500)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
ensurePrepInterior()
|
||||
while not exports.devx_core:IsPlayerLoaded() do Wait(500) end
|
||||
refreshStatus()
|
||||
spawnPlanners()
|
||||
for _, entry in ipairs(DevXCrimeConfig.PrepLocations) do
|
||||
local blip = AddBlipForCoord(entry.exterior.x, entry.exterior.y, entry.exterior.z)
|
||||
SetBlipSprite(blip,478);SetBlipScale(blip,.65);SetBlipColour(blip,47);SetBlipAsShortRange(blip,true)
|
||||
BeginTextCommandSetBlipName('STRING');AddTextComponentString('Bankraub-Vorbereitung');EndTextCommandSetBlipName(blip)
|
||||
end
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local sleep = 700
|
||||
if exports.devx_core:IsPlayerLoaded() and not robberyRunning and not cutsceneRunning then
|
||||
local coords = GetEntityCoords(PlayerPedId())
|
||||
if activePrep then
|
||||
local objective = activePrep.objective
|
||||
local objectiveDistance = #(coords - vector3(objective.x, objective.y, objective.z))
|
||||
local exit = DevXCrimeConfig.PrepInterior.exit
|
||||
local exitDistance = #(coords - exit)
|
||||
if objectiveDistance < 15.0 then
|
||||
sleep = 0;drawMarkerAt(objective,70,150,245)
|
||||
if objectiveDistance < 1.6 then
|
||||
help(('~INPUT_CONTEXT~ %s'):format(activePrep.objectiveLabel))
|
||||
if IsControlJustReleased(0,38) then
|
||||
timedAction(activePrep.objectiveLabel,7)
|
||||
DevXTriggerCallback('crime:completePrep',{key=activePrep.key},function(result)
|
||||
if result.ok then
|
||||
progress=result.progress;removeObjectiveProp();deletePeds(warehousePeds);deleteProps(missionProps);deleteVehicles(missionVehicles);activePrep=nil;teleport(result.exterior);TriggerEvent('devx_ipl:client:restoreWorld', 'crime-complete')
|
||||
notify('Vorbereitungsmission abgeschlossen.')
|
||||
else notify(result.error or 'Mission fehlgeschlagen.') end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
if exitDistance < 8.0 then
|
||||
sleep=0;drawMarkerAt(exit,230,230,230)
|
||||
if exitDistance < 1.6 then
|
||||
help('~INPUT_CONTEXT~ Lager verlassen')
|
||||
if IsControlJustReleased(0,38) then leavePrep() end
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, entry in ipairs(DevXCrimeConfig.PrepLocations) do
|
||||
local distance = #(coords - vector3(entry.exterior.x,entry.exterior.y,entry.exterior.z))
|
||||
if distance < 18.0 then
|
||||
sleep=0;drawMarkerAt(entry.exterior,progress[entry.key] and 70 or 230,progress[entry.key] and 190 or 150,90)
|
||||
if distance < 1.8 then
|
||||
if progress[entry.key] then help(('Vorbereitung abgeschlossen: %s'):format(entry.label))
|
||||
else
|
||||
help(('~INPUT_CONTEXT~ mit %s planen: %s'):format(DevXCrimeConfig.PlannerName, entry.label))
|
||||
if IsControlJustReleased(0,38) then beginPrep(entry) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local bankDistance=#(coords-DevXCrimeConfig.Bank)
|
||||
if bankDistance<18.0 then
|
||||
sleep=0;drawMarkerAt(DevXCrimeConfig.Bank,210,55,75)
|
||||
if bankDistance<1.8 then
|
||||
help('~INPUT_CONTEXT~ vorbereiteten Bankraub starten')
|
||||
if IsControlJustReleased(0,38) then
|
||||
DevXTriggerCallback('crime:startRobbery',{},function(result)
|
||||
if not result.ok then notify(result.error or 'Bankraub konnte nicht gestartet werden.');return end
|
||||
robberyRunning=true
|
||||
CreateThread(function()
|
||||
timedAction('Tresor wird geöffnet …',result.duration)
|
||||
DevXTriggerCallback('crime:finishRobbery',{},function(finish)
|
||||
robberyRunning=false
|
||||
if finish.ok then progress={drill=false,card=false,vehicle=false};notify(('Bankraub erfolgreich: %s $ und %s RP-XP.'):format(finish.reward,finish.xp))
|
||||
else notify(finish.error or 'Bankraub fehlgeschlagen.') end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Wait(sleep)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop',function(resourceName)
|
||||
if resourceName~=GetCurrentResourceName() then return end
|
||||
removeObjectiveProp();deletePeds(plannerPeds);deletePeds(warehousePeds);deleteProps(missionProps);deleteVehicles(missionVehicles);TriggerServerEvent('devx_core:server:returnToWorld');TriggerEvent('devx_ipl:client:restoreWorld', 'crime-stop')
|
||||
end)
|
||||
@@ -0,0 +1,10 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
author 'Colin-Joel / DevX Studios'
|
||||
description 'DevX RP bank robbery with preparation missions'
|
||||
version '0.4.3'
|
||||
lua54 'yes'
|
||||
shared_scripts { '@devx_core/shared/config.lua', 'shared/config.lua' }
|
||||
client_scripts { '@devx_core/client/callbacks.lua', 'client/main.lua' }
|
||||
server_scripts { '@oxmysql/lib/MySQL.lua', '@devx_core/server/callbacks.lua', 'server/main.lua' }
|
||||
dependencies { 'devx_core', 'devx_banking', 'devx_ipl' }
|
||||
@@ -0,0 +1,165 @@
|
||||
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)
|
||||
@@ -0,0 +1,90 @@
|
||||
DevXCrimeConfig = {
|
||||
MissionBucketBase = 20000,
|
||||
|
||||
-- v0.4.3 deliberately uses the always-available 10-car garage interior.
|
||||
-- It needs no IPL and therefore cannot appear as an empty warehouse when a
|
||||
-- DLC placement fails to stream. Mission props/NPCs create the warehouse set.
|
||||
PrepInterior = {
|
||||
ipls = {},
|
||||
spawn = vector4(239.86, -1004.71, -99.00, 88.5),
|
||||
exit = vector3(239.55, -1004.55, -99.00),
|
||||
loadAt = vector3(229.96, -981.79, -99.66),
|
||||
setDressing = {
|
||||
{ model = 'prop_boxpile_07d', coords = vector4(233.60, -1003.00, -99.65, 90.0) },
|
||||
{ model = 'prop_boxpile_06b', coords = vector4(233.55, -999.55, -99.65, 90.0) },
|
||||
{ model = 'prop_tool_bench02', coords = vector4(224.25, -1001.50, -99.65, 270.0) },
|
||||
{ model = 'prop_crate_11e', coords = vector4(225.90, -998.80, -99.65, 0.0) },
|
||||
{ model = 'prop_generator_03b', coords = vector4(228.20, -997.30, -99.65, 180.0) },
|
||||
{ model = 'prop_worklight_03b', coords = vector4(229.70, -1003.20, -99.65, 90.0) },
|
||||
{ model = 'prop_shelves_02', coords = vector4(222.40, -998.10, -99.65, 270.0) },
|
||||
{ model = 'prop_shelves_02', coords = vector4(222.40, -1001.20, -99.65, 270.0) },
|
||||
{ model = 'prop_box_wood05a', coords = vector4(236.40, -998.20, -99.65, 0.0) },
|
||||
{ model = 'prop_cratepile_07a', coords = vector4(236.25, -1001.60, -99.65, 180.0) },
|
||||
{ model = 'prop_toolchest_05', coords = vector4(225.10, -996.65, -99.65, 90.0) },
|
||||
{ model = 'prop_barrel_02a', coords = vector4(238.00, -995.80, -99.65, 0.0) },
|
||||
{ model = 'prop_barrel_02a', coords = vector4(238.00, -994.90, -99.65, 0.0) },
|
||||
{ model = 'prop_worklight_04b', coords = vector4(222.95, -1003.55, -99.65, 270.0) },
|
||||
{ model = 'prop_folding_chair_01', coords = vector4(228.40, -1003.80, -99.65, 180.0) },
|
||||
{ model = 'prop_table_03', coords = vector4(230.00, -1003.85, -99.65, 0.0) }
|
||||
},
|
||||
vehicles = {
|
||||
{ model = 'burrito3', coords = vector4(235.25, -988.10, -99.45, 180.0) },
|
||||
{ model = 'sultan', coords = vector4(229.80, -988.10, -99.45, 180.0) }
|
||||
}
|
||||
},
|
||||
PlannerModel = 's_m_m_highsec_01',
|
||||
PlannerName = 'Vincent',
|
||||
PrepLocations = {
|
||||
{
|
||||
key = 'drill',
|
||||
label = 'Bohrgerät am Hafen beschaffen',
|
||||
exterior = vector4(1204.15, -3116.62, 5.54, 90.0),
|
||||
planner = vector4(1202.40, -3115.15, 5.54, 178.0),
|
||||
objective = vector4(224.45, -1001.20, -99.65, 270.0),
|
||||
objectiveLabel = 'Industriebohrer von der Werkbank nehmen',
|
||||
prop = 'prop_tool_drill',
|
||||
dialogue = {
|
||||
'Der Tresor braucht mehr als rohe Gewalt.',
|
||||
'Unser Werkzeug liegt in einer abgeschirmten Garage.',
|
||||
'Hol den Industriebohrer und verschwinde unauffällig.'
|
||||
}
|
||||
},
|
||||
{
|
||||
key = 'card',
|
||||
label = 'Zugangskarte aus dem Lager holen',
|
||||
exterior = vector4(707.31, -966.86, 30.41, 180.0),
|
||||
planner = vector4(705.55, -966.10, 30.41, 260.0),
|
||||
objective = vector4(226.10, -998.75, -99.65, 0.0),
|
||||
objectiveLabel = 'Sicherheitskarte aus der Transportkiste nehmen',
|
||||
prop = 'prop_cs_swipe_card',
|
||||
dialogue = {
|
||||
'Die Banktür prüft eine Mitarbeiterkarte.',
|
||||
'Unser Kontakt hat eine Karte in einer Kiste deponiert.',
|
||||
'Nimm sie und hinterlasse keine Spuren.'
|
||||
}
|
||||
},
|
||||
{
|
||||
key = 'vehicle',
|
||||
label = 'Fluchtfahrzeug vorbereiten',
|
||||
exterior = vector4(970.25, -1832.05, 31.11, 175.0),
|
||||
planner = vector4(968.65, -1831.40, 31.11, 260.0),
|
||||
objective = vector4(228.35, -997.15, -99.65, 180.0),
|
||||
objectiveLabel = 'Fahrzeugsteuergerät am Laptop programmieren',
|
||||
prop = 'prop_laptop_01a',
|
||||
dialogue = {
|
||||
'Ohne sauberes Fluchtfahrzeug endet der Plan an der ersten Kreuzung.',
|
||||
'Programmiere das Steuergerät auf unsere Kennung.',
|
||||
'Danach ist das Fahrzeug für das Finale vorbereitet.'
|
||||
}
|
||||
}
|
||||
},
|
||||
WarehouseWorkers = {
|
||||
{ model = 's_m_m_dockwork_01', coords = vector4(232.20, -997.80, -99.65, 180.0), scenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||||
{ model = 's_m_y_dockwork_01', coords = vector4(225.10, -1003.35, -99.65, 270.0), scenario = 'WORLD_HUMAN_WELDING' }
|
||||
},
|
||||
Bank = vector3(146.74, -1044.82, 29.37),
|
||||
RobberyDurationSeconds = 75,
|
||||
GlobalCooldownSeconds = 3600,
|
||||
MinimumReward = 4500,
|
||||
MaximumReward = 8500
|
||||
}
|
||||
Reference in New Issue
Block a user