ESX = exports['es_extended']:getSharedObject() local spawnedPeds = {} local isNuiOpen = false local currentStation = nil -- ===================== -- DRAW TEXT 3D -- ===================== function DrawText3D(x, y, z, text) SetTextScale(0.35, 0.35) SetTextFont(4) SetTextProportional(1) SetTextColour(255, 255, 255, 215) SetTextEntry('STRING') SetTextCentre(true) AddTextComponentString(text) SetDrawOrigin(x, y, z, 0) DrawText(0.0, 0.0) local factor = (string.len(text)) / 370 DrawRect(0.0, 0.0 + 0.0125, 0.017 + factor, 0.03, 0, 0, 0, 100) ClearDrawOrigin() end -- ===================== -- NPC SPAWNING -- ===================== Citizen.CreateThread(function() for stationId, station in pairs(Config.CraftingStations) do local npc = station.npc local hash = GetHashKey(npc.model) RequestModel(hash) while not HasModelLoaded(hash) do Citizen.Wait(10) end local ped = CreatePed(4, hash, npc.coords.x, npc.coords.y, npc.coords.z - 1.0, npc.coords.w, false, true) SetEntityHeading(ped, npc.coords.w) FreezeEntityPosition(ped, true) SetEntityInvincible(ped, true) SetBlockingOfNonTemporaryEvents(ped, true) if npc.scenario then TaskStartScenarioInPlace(ped, npc.scenario, 0, true) end spawnedPeds[stationId] = ped SetModelAsNoLongerNeeded(hash) end end) -- Cleanup on resource stop AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end for _, ped in pairs(spawnedPeds) do if DoesEntityExist(ped) then DeleteEntity(ped) end end end) -- ===================== -- NPC INTERACTION -- ===================== Citizen.CreateThread(function() while true do local sleep = 1000 local playerCoords = GetEntityCoords(PlayerPedId()) local closestStation = nil local closestDist = Config.InteractionDistance + 1 for stationId, station in pairs(Config.CraftingStations) do local dist = #(playerCoords - vector3(station.npc.coords.x, station.npc.coords.y, station.npc.coords.z)) if dist < closestDist then closestDist = dist closestStation = stationId end end if closestStation and closestDist <= Config.InteractionDistance then sleep = 0 local station = Config.CraftingStations[closestStation] if not isNuiOpen then -- Draw 3D text above NPC instead of spamming notifications local npcCoords = station.npc.coords DrawText3D(npcCoords.x, npcCoords.y, npcCoords.z + 1.0, '[E] ' .. station.label) if IsControlJustReleased(0, 38) then -- E key OpenCraftingMenu(closestStation) end end end Citizen.Wait(sleep) end end) -- ===================== -- VEHICLE DETECTION -- ===================== function GetOwnedVehiclesNearby(cb) local playerCoords = GetEntityCoords(PlayerPedId()) local vehicles = GetGamePool('CVehicle') local nearbyOwned = {} local checksRemaining = 0 for _, vehicle in ipairs(vehicles) do local vehCoords = GetEntityCoords(vehicle) local dist = #(playerCoords - vehCoords) if dist <= Config.VehicleSearchRadius then local plate = string.gsub(GetVehicleNumberPlateText(vehicle), '%s+', '') checksRemaining = checksRemaining + 1 ESX.TriggerServerCallback('mercyv-crafting:checkVehicleOwnership', function(isOwned) if isOwned then nearbyOwned[#nearbyOwned + 1] = { vehicle = vehicle, plate = plate, model = GetDisplayNameFromVehicleModel(GetEntityModel(vehicle)), } end checksRemaining = checksRemaining - 1 if checksRemaining <= 0 then cb(nearbyOwned) end end, plate) end end if checksRemaining == 0 then cb(nearbyOwned) end end -- ===================== -- NUI MANAGEMENT -- ===================== function OpenCraftingMenu(stationId) if isNuiOpen then return end currentStation = stationId local station = Config.CraftingStations[stationId] -- Get station recipes local recipes = {} for _, recipeId in ipairs(station.recipes) do local recipe = Config.Recipes[recipeId] if recipe then recipes[#recipes + 1] = { id = recipeId, label = recipe.label, category = recipe.category, result = recipe.result, ingredients = recipe.ingredients, duration = recipe.duration, } end end -- Get player inventory ESX.TriggerServerCallback('mercyv-crafting:getPlayerItems', function(playerItems) -- Get nearby owned vehicles and their trunk items GetOwnedVehiclesNearby(function(vehicles) local vehicleData = {} if #vehicles > 0 then local trunkChecks = #vehicles for _, veh in ipairs(vehicles) do ESX.TriggerServerCallback('mercyv-crafting:getTrunkItems', function(trunkItems) vehicleData[#vehicleData + 1] = { plate = veh.plate, model = veh.model, items = trunkItems, } trunkChecks = trunkChecks - 1 if trunkChecks <= 0 then SendCraftingData(station, recipes, playerItems, vehicleData) end end, veh.plate) end else SendCraftingData(station, recipes, playerItems, vehicleData) end end) end) end function SendCraftingData(station, recipes, playerItems, vehicleData) isNuiOpen = true SetNuiFocus(true, true) SendNUIMessage({ type = 'open', stationLabel = station.label, stationImage = station.image or '', stationColor = station.color or '', recipes = recipes, playerItems = playerItems, vehicles = vehicleData, }) -- Check if there are active crafts running at this station ESX.TriggerServerCallback('mercyv-crafting:getCraftStatus', function(statuses) if statuses then SendNUIMessage({ type = 'craftStatuses', statuses = statuses, }) end end) end function CloseCraftingMenu() if not isNuiOpen then return end isNuiOpen = false currentStation = nil SetNuiFocus(false, false) SendNUIMessage({ type = 'close' }) end -- ===================== -- NUI CALLBACKS -- ===================== RegisterNUICallback('close', function(_, cb) CloseCraftingMenu() cb('ok') end) RegisterNUICallback('startCraft', function(data, cb) if not currentStation then cb({ success = false, message = 'Keine Station ausgewaehlt' }) return end TriggerServerEvent('mercyv-crafting:startCraft', data.recipeId, currentStation, data.useSource, data.craftAmount or 1) cb('ok') end) RegisterNUICallback('collectCraft', function(data, cb) TriggerServerEvent('mercyv-crafting:collectCraft', data.recipeId, data.collectTo, data.collectAmount or 1) cb('ok') end) -- ===================== -- SERVER EVENT HANDLERS -- ===================== RegisterNetEvent('mercyv-crafting:craftResult') AddEventHandler('mercyv-crafting:craftResult', function(success, message) if success then TriggerEvent('hex_4_hud:notify', 'Crafting', message, 'success', 3000) else TriggerEvent('hex_4_hud:notify', 'Crafting', message, 'error', 3000) SendNUIMessage({ type = 'craftingFailed', message = message }) end end) RegisterNetEvent('mercyv-crafting:craftStatusUpdate') AddEventHandler('mercyv-crafting:craftStatusUpdate', function(status) SendNUIMessage({ type = 'craftStatusUpdate', status = status }) end) RegisterNetEvent('mercyv-crafting:collectResult') AddEventHandler('mercyv-crafting:collectResult', function(success, message, recipeId) if success then TriggerEvent('hex_4_hud:notify', 'Crafting', message, 'success', 3000) SendNUIMessage({ type = 'collectSuccess', recipeId = recipeId }) else TriggerEvent('hex_4_hud:notify', 'Crafting', message, 'error', 3000) end end) CreateThread(function() for k, station in pairs(Config.CraftingStations) do if station.blip and station.blip.enabled then local blip = AddBlipForCoord(station.npc.coords.x, station.npc.coords.y, station.npc.coords.z) SetBlipSprite(blip, station.blip.sprite or 1) SetBlipDisplay(blip, 4) SetBlipScale(blip, station.blip.scale or 1.0) SetBlipColour(blip, station.blip.color or 1) SetBlipAsShortRange(blip, true) BeginTextCommandSetBlipName("STRING") AddTextComponentString(station.label) EndTextCommandSetBlipName(blip) end end end)