2026-04-14 15:54:53 +02:00

423 lines
13 KiB
Lua

ESX = exports['es_extended']:getSharedObject()
local isNuiOpen = false
local currentFactionId = nil
local pendingInvite = nil
local isGarageOpen = false
local spawnedNpcs = {} -- key -> ped handle
local playerLoaded = false
-- PlayerData laden (event-basiert, kein Polling)
Citizen.CreateThread(function()
-- Einmaliger Check bei ensure/restart (Spieler ist evtl. schon geladen)
local pd = ESX.GetPlayerData()
if pd and pd.job and pd.job.name then
ESX.PlayerData = pd
playerLoaded = true
end
end)
RegisterNetEvent('esx:playerLoaded')
AddEventHandler('esx:playerLoaded', function(xPlayer)
ESX.PlayerData = xPlayer
playerLoaded = true
end)
-- =====================
-- NPC SPAWNING
-- =====================
function SpawnNPC(key, coords, model, scenario)
if spawnedNpcs[key] then return end
local hash = GetHashKey(model)
RequestModel(hash)
while not HasModelLoaded(hash) do Citizen.Wait(10) end
local ped = CreatePed(4, hash, coords.x, coords.y, coords.z - 1.0, coords.w, false, true)
SetEntityHeading(ped, coords.w)
FreezeEntityPosition(ped, true)
SetEntityInvincible(ped, true)
SetBlockingOfNonTemporaryEvents(ped, true)
TaskStartScenarioInPlace(ped, scenario, 0, true)
SetModelAsNoLongerNeeded(hash)
spawnedNpcs[key] = ped
end
-- Spawn NPCs fuer alle Fraktionen bei Resource-Start
Citizen.CreateThread(function()
Citizen.Wait(2000)
for factionId, config in pairs(Config.Factions) do
-- Garage NPC
if config.garageNpc and config.garage then
local model = config.garageNpcModel or Config.DefaultGarageNpcModel
local scenario = config.garageNpcScenario or Config.DefaultGarageNpcScenario
SpawnNPC('garage_' .. factionId, config.garage, model, scenario)
end
-- Stash NPC
if config.stashNpc and config.stash then
local model = config.stashNpcModel or Config.DefaultStashNpcModel
local scenario = config.stashNpcScenario or Config.DefaultStashNpcScenario
SpawnNPC('stash_' .. factionId, config.stash, model, scenario)
end
end
end)
-- Cleanup on resource stop
AddEventHandler('onResourceStop', function(resourceName)
if resourceName ~= GetCurrentResourceName() then return end
for _, ped in pairs(spawnedNpcs) do
if DoesEntityExist(ped) then DeleteEntity(ped) end
end
end)
-- =====================
-- INTERACTION THREAD (Computer + Garage + Stash)
-- =====================
Citizen.CreateThread(function()
-- Warte bis ESX PlayerData geladen ist
while not playerLoaded do
Citizen.Wait(500)
end
local nearPoint = false
local COARSE_DIST = 50.0
while true do
local sleep = 1000
local playerCoords = GetEntityCoords(PlayerPedId())
local foundNear = false
local playerJob = ESX.PlayerData.job
if not isNuiOpen and not isGarageOpen and playerJob and playerJob.name then
local factionConfig = Config.Factions[playerJob.name]
if factionConfig then
-- Computer Check
if factionConfig.computer then
local compCoords = factionConfig.computer
local dist = #(playerCoords - vector3(compCoords.x, compCoords.y, compCoords.z))
if dist <= Config.InteractionDistance then
if playerJob.grade >= (factionConfig.minComputerGrade or 0) then
sleep = 0
foundNear = true
exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Computer', 'E')
if IsControlJustReleased(0, 38) then
OpenFactionMenu(playerJob.name)
end
end
end
end
-- Garage Check (separater Standort)
if not foundNear and factionConfig.garage then
local garageCoords = factionConfig.garage
local dist = #(playerCoords - vector3(garageCoords.x, garageCoords.y, garageCoords.z))
if dist <= Config.InteractionDistance then
sleep = 0
foundNear = true
exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Garage', 'E')
if IsControlJustReleased(0, 38) then
OpenGarageMenu(playerJob.name)
end
end
end
-- Stash Check (separater Standort)
if not foundNear and factionConfig.stash then
local stashCoords = factionConfig.stash
local dist = #(playerCoords - vector3(stashCoords.x, stashCoords.y, stashCoords.z))
if dist <= Config.InteractionDistance then
sleep = 0
foundNear = true
exports[Config.HelpNotify.export][Config.HelpNotify.showFunc]('Fraktions-Lager', 'E')
if IsControlJustReleased(0, 38) then
OpenFactionStash(playerJob.name)
end
end
end
end
end
if not foundNear and nearPoint then
exports[Config.HelpNotify.export][Config.HelpNotify.hideFunc]()
end
nearPoint = foundNear
Citizen.Wait(sleep)
end
end)
-- =====================
-- NUI MANAGEMENT
-- =====================
function OpenFactionMenu(factionId)
if isNuiOpen then return end
ESX.TriggerServerCallback('mercyv-fraks:getFactionData', function(data)
if not data then return end
isNuiOpen = true
currentFactionId = factionId
SetNuiFocus(true, true)
SendNUIMessage({
type = 'open',
factionId = data.factionId,
factionLabel = data.factionLabel,
factionImage = data.factionImage or '',
playerGrade = data.playerGrade,
grades = data.grades,
treasury = data.treasury,
totalMembers = data.totalMembers,
onlineMembers = data.onlineMembers,
permissions = data.permissions,
permissionValues = data.permissionValues,
permissionLabels = Config.PermissionLabels,
theme = data.theme,
vehicles = data.vehicles,
})
end)
end
function CloseMenu()
if not isNuiOpen then return end
isNuiOpen = false
currentFactionId = nil
SetNuiFocus(false, false)
SendNUIMessage({ type = 'close' })
end
-- =====================
-- NUI CALLBACKS
-- =====================
RegisterNUICallback('close', function(_, cb)
CloseMenu()
cb('ok')
end)
RegisterNUICallback('getMembers', function(_, cb)
ESX.TriggerServerCallback('mercyv-fraks:getMembers', function(members)
SendNUIMessage({ type = 'updateMembers', members = members })
end)
cb('ok')
end)
RegisterNUICallback('getTreasuryLog', function(_, cb)
ESX.TriggerServerCallback('mercyv-fraks:getTreasuryLog', function(log)
SendNUIMessage({ type = 'updateTreasuryLog', log = log })
end)
cb('ok')
end)
RegisterNUICallback('getNearbyPlayers', function(_, cb)
ESX.TriggerServerCallback('mercyv-fraks:getNearbyPlayers', function(players)
SendNUIMessage({ type = 'updateNearbyPlayers', players = players })
end)
cb('ok')
end)
RegisterNUICallback('inviteMember', function(data, cb)
TriggerServerEvent('mercyv-fraks:inviteMember', data.targetSource)
cb('ok')
end)
RegisterNUICallback('kickMember', function(data, cb)
TriggerServerEvent('mercyv-fraks:kickMember', data.identifier)
cb('ok')
end)
RegisterNUICallback('changeRank', function(data, cb)
TriggerServerEvent('mercyv-fraks:changeRank', data.identifier, data.grade)
cb('ok')
end)
RegisterNUICallback('depositTreasury', function(data, cb)
TriggerServerEvent('mercyv-fraks:depositTreasury', data.amount, data.accountType)
cb('ok')
end)
RegisterNUICallback('withdrawTreasury', function(data, cb)
TriggerServerEvent('mercyv-fraks:withdrawTreasury', data.amount, data.accountType)
cb('ok')
end)
RegisterNUICallback('garageSpawn', function(data, cb)
TriggerServerEvent('mercyv-fraks:garageSpawn', data.model)
cb('ok')
end)
RegisterNUICallback('garageStore', function(_, cb)
TriggerServerEvent('mercyv-fraks:garageStore')
cb('ok')
end)
RegisterNUICallback('closeGarage', function(_, cb)
CloseGarage()
cb('ok')
end)
RegisterNUICallback('updatePermission', function(data, cb)
TriggerServerEvent('mercyv-fraks:updatePermission', data.permKey, data.minGrade)
cb('ok')
end)
-- =====================
-- SERVER EVENT HANDLERS
-- =====================
RegisterNetEvent('mercyv-fraks:notify')
AddEventHandler('mercyv-fraks:notify', function(message, nType)
TriggerEvent(Config.Notification.event, Config.Notification.title, message, nType or 'info', Config.Notification.duration)
end)
RegisterNetEvent('mercyv-fraks:updateTreasury')
AddEventHandler('mercyv-fraks:updateTreasury', function(bal)
if isNuiOpen then
SendNUIMessage({ type = 'updateTreasury', treasury = bal })
end
end)
RegisterNetEvent('mercyv-fraks:forceClose')
AddEventHandler('mercyv-fraks:forceClose', function()
CloseMenu()
end)
-- =====================
-- INVITE SYSTEM (Client-Side)
-- =====================
RegisterNetEvent('mercyv-fraks:receiveInvite')
AddEventHandler('mercyv-fraks:receiveInvite', function(inviteData)
pendingInvite = inviteData
SetNuiFocus(true, true)
SendNUIMessage({
type = 'showInvite',
factionLabel = inviteData.factionLabel,
inviterName = inviteData.inviterName,
})
end)
RegisterNUICallback('respondInvite', function(data, cb)
SetNuiFocus(false, false)
if pendingInvite then
TriggerServerEvent('mercyv-fraks:respondInvite', data.accepted, pendingInvite.factionId, pendingInvite.inviterSource)
pendingInvite = nil
end
cb('ok')
end)
-- =====================
-- GARAGE FUNCTIONS
-- =====================
function OpenFactionStash(factionId)
if isNuiOpen or isGarageOpen then return end
-- Rechte-Check ueber Server
ESX.TriggerServerCallback('mercyv-fraks:canUseStash', function(allowed)
if not allowed then
TriggerEvent(Config.Notification.event, Config.Notification.title, 'Kein Zugriff auf das Lager', 'error', Config.Notification.duration)
return
end
local config = Config.Factions[factionId]
local stashId = 'faction_stash_' .. factionId
local slots = config.stashSlots or 50
local weight = config.stashWeight or 100000
TriggerServerEvent('codem-inventory:server:openstash', stashId, slots, weight, config.label .. ' Lager')
end)
end
function OpenGarageMenu(factionId)
if isGarageOpen or isNuiOpen then return end
ESX.TriggerServerCallback('mercyv-fraks:getGarageData', function(data)
if not data then return end
isGarageOpen = true
SetNuiFocus(true, true)
SendNUIMessage({
type = 'openGarage',
vehicles = data.vehicles,
theme = data.theme,
factionLabel = data.factionLabel,
hasSpawned = data.hasSpawned,
})
end)
end
function CloseGarage()
if not isGarageOpen then return end
isGarageOpen = false
SetNuiFocus(false, false)
SendNUIMessage({ type = 'closeGarage' })
end
-- =====================
-- VEHICLE SPAWNING (Client-Side)
-- =====================
RegisterNetEvent('mercyv-fraks:doSpawnVehicle')
AddEventHandler('mercyv-fraks:doSpawnVehicle', function(model, coords, color, livery)
local hash = GetHashKey(model)
RequestModel(hash)
local timeout = 0
while not HasModelLoaded(hash) and timeout < 50 do
Citizen.Wait(100)
timeout = timeout + 1
end
if not HasModelLoaded(hash) then
TriggerEvent(Config.Notification.event, Config.Notification.title, 'Fahrzeugmodell nicht gefunden', 'error', Config.Notification.duration)
return
end
-- ✅ SPAWN FIX
local vehicle = CreateVehicle(hash, coords.x, coords.y, coords.z, coords.w, true, false)
SetModelAsNoLongerNeeded(hash)
SetVehicleOnGroundProperly(vehicle)
SetVehicleModKit(vehicle, 0)
Citizen.Wait(200)
-- 🎨 Farbe
if color and color.primary then
SetVehicleColours(vehicle, color.primary, color.secondary or color.primary)
end
-- 🔥 LIVERY
if livery ~= nil and livery >= 0 then
SetVehicleLivery(vehicle, livery) -- Standard-Livery
SetVehicleMod(vehicle, 48, livery - 1) -- Mod-basierte Livery (die meisten Custom-Fahrzeuge)
end
-- Netzwerk + Keys
local netId = NetworkGetNetworkIdFromEntity(vehicle)
local plate = GetVehicleNumberPlateText(vehicle)
TriggerServerEvent('mercyv-fraks:vehicleSpawned', netId, plate)
-- Spieler reinsetzen
TaskWarpPedIntoVehicle(PlayerPedId(), vehicle, -1)
-- Garage schließen
CloseGarage()
end)
RegisterNetEvent('mercyv-fraks:despawnVehicle')
AddEventHandler('mercyv-fraks:despawnVehicle', function(netId)
local vehicle = NetworkGetEntityFromNetworkId(netId)
if vehicle and DoesEntityExist(vehicle) then
DeleteEntity(vehicle)
end
end)
-- ESX PlayerData sync
RegisterNetEvent('esx:setJob')
AddEventHandler('esx:setJob', function(job)
ESX.PlayerData.job = job
playerLoaded = true
end)