ESX = exports['es_extended']:getSharedObject() local spawnedPeds = {} -- shop_id -> ped handle local spawnedBlips = {} -- shop_id -> blip handle local shopOwnership = {} -- shop_id -> { owner_identifier, shop_name } local isNuiOpen = false local currentShopId = nil local currentViewMode = 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 -- ===================== function SpawnShopNPC(shopId, shopConfig) if spawnedPeds[shopId] then return end local npc = shopConfig.npc local hash = GetHashKey(shopConfig.npcModel or Config.DefaultNpcModel) RequestModel(hash) while not HasModelLoaded(hash) do Citizen.Wait(10) end local ped = CreatePed(4, hash, npc.x, npc.y, npc.z - 1.0, npc.w, false, true) SetEntityHeading(ped, npc.w) FreezeEntityPosition(ped, true) SetEntityInvincible(ped, true) SetBlockingOfNonTemporaryEvents(ped, true) TaskStartScenarioInPlace(ped, Config.DefaultNpcScenario, 0, true) SetModelAsNoLongerNeeded(hash) spawnedPeds[shopId] = ped end function SpawnAllShopNPCs() for shopId, shopConfig in pairs(Config.Shops) do SpawnShopNPC(shopId, shopConfig) end end function UpdateBlips() -- Remove old blips for shopId, blip in pairs(spawnedBlips) do RemoveBlip(blip) spawnedBlips[shopId] = nil end -- Create new blips for shopId, shopConfig in pairs(Config.Shops) do if shopConfig.blip then local npc = shopConfig.npc local blip = AddBlipForCoord(npc.x, npc.y, npc.z) local isWeapon = shopConfig.shopType == 'weapon' SetBlipSprite(blip, shopConfig.blip.sprite or 52) SetBlipDisplay(blip, 4) SetBlipScale(blip, shopConfig.blip.scale or 0.7) SetBlipAsShortRange(blip, true) if not isWeapon then SetBlipCategory(blip, 7) end -- Color: green = owned, red = unowned/for sale, weapon shops always red if isWeapon then SetBlipColour(blip, shopConfig.blip.color or 1) else local ownership = shopOwnership[shopId] if ownership and ownership.owner_identifier then SetBlipColour(blip, 2) -- green else SetBlipColour(blip, 1) -- red end end BeginTextCommandSetBlipName("STRING") AddTextComponentString(isWeapon and "Ammunition" or "24/7 Shop") EndTextCommandSetBlipName(blip) spawnedBlips[shopId] = blip end end end -- ===================== -- SHOP SYNC -- ===================== RegisterNetEvent('mercyv-shops:syncShops') AddEventHandler('mercyv-shops:syncShops', function(serverShops) shopOwnership = {} for shopId, data in pairs(serverShops) do shopOwnership[shopId] = { owner_identifier = data.owner_identifier, owner_name = data.owner_name, shop_name = data.shop_name, } end UpdateBlips() 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 for _, blip in pairs(spawnedBlips) do RemoveBlip(blip) end end) -- Spawn NPCs and request sync on start Citizen.CreateThread(function() SpawnAllShopNPCs() Citizen.Wait(2000) TriggerServerEvent('mercyv-shops:requestSync') end) -- ===================== -- NPC INTERACTION -- ===================== Citizen.CreateThread(function() local nearShop = false local nearbyShopId = nil local nearbyType = nil -- 'npc' oder 'mgmt' local COARSE_DIST = Config.CoarseDistance or 50.0 -- Grobe Distanz für Vorfilter while true do local sleep = 1000 local playerCoords = GetEntityCoords(PlayerPedId()) local foundNear = false if not isNuiOpen then -- Wenn wir bereits einen nahegelegenen Shop kennen, nur diesen pruefen if nearbyShopId then local shopConfig = Config.Shops[nearbyShopId] if shopConfig then local npcDist = #(playerCoords - vector3(shopConfig.npc.x, shopConfig.npc.y, shopConfig.npc.z)) local mgmtDist = #(playerCoords - vector3(shopConfig.management.x, shopConfig.management.y, shopConfig.management.z)) local isWeaponShop = shopConfig.shopType == 'weapon' if npcDist <= Config.InteractionDistance then sleep = 0 foundNear = true local ownership = shopOwnership[nearbyShopId] local displayName = (ownership and ownership.shop_name) or shopConfig.label exports['hex_4_hud']:ShowHelpNotify(displayName, 'E') if IsControlJustReleased(0, 38) then OpenCustomerMenu(nearbyShopId) end elseif not isWeaponShop and mgmtDist <= Config.InteractionDistance then sleep = 0 foundNear = true exports['hex_4_hud']:ShowHelpNotify('Verwaltung', 'E') if IsControlJustReleased(0, 38) then OpenManagementMenu(nearbyShopId) end elseif npcDist > COARSE_DIST and (isWeaponShop or mgmtDist > COARSE_DIST) then -- Zu weit weg, Shop vergessen nearbyShopId = nil end end end -- Wenn kein nahegelegener Shop bekannt, alle durchsuchen (nur bei sleep=1000) if not nearbyShopId and not foundNear then for shopId, shopConfig in pairs(Config.Shops) do local npcDist = #(playerCoords - vector3(shopConfig.npc.x, shopConfig.npc.y, shopConfig.npc.z)) if npcDist <= COARSE_DIST then nearbyShopId = shopId if npcDist <= Config.InteractionDistance then sleep = 0 foundNear = true local ownership = shopOwnership[shopId] local displayName = (ownership and ownership.shop_name) or shopConfig.label exports['hex_4_hud']:ShowHelpNotify(displayName, 'E') if IsControlJustReleased(0, 38) then OpenCustomerMenu(shopId) end end break end -- Waffen-Shops haben keinen Management-Punkt if shopConfig.shopType ~= 'weapon' then local mgmtDist = #(playerCoords - vector3(shopConfig.management.x, shopConfig.management.y, shopConfig.management.z)) if mgmtDist <= COARSE_DIST then nearbyShopId = shopId if mgmtDist <= Config.InteractionDistance then sleep = 0 foundNear = true exports['hex_4_hud']:ShowHelpNotify('Verwaltung', 'E') if IsControlJustReleased(0, 38) then OpenManagementMenu(shopId) end end break end end end end if not foundNear and nearShop then exports['hex_4_hud']:HideHelpNotify() end nearShop = foundNear end Citizen.Wait(sleep) end end) -- ===================== -- NUI MANAGEMENT -- ===================== function OpenCustomerMenu(shopId) if isNuiOpen then return end ESX.TriggerServerCallback('mercyv-shops:getShelfItems', function(shelfItems, lockedCategories) local shopConfig = Config.Shops[shopId] local ownership = shopOwnership[shopId] local displayName = (ownership and ownership.shop_name) or shopConfig.label local isNpc = not ownership or not ownership.owner_identifier local isWeapon = shopConfig.shopType == 'weapon' isNuiOpen = true currentShopId = shopId currentViewMode = 'customer' SetNuiFocus(true, true) SendNUIMessage({ type = 'openCustomer', shopId = shopId, shopName = displayName, shelves = shelfItems, isNpcShop = isNpc or isWeapon, theme = Config.Theme, categories = isWeapon and Config.WeaponCategories or Config.Categories, lockedCategories = lockedCategories or {}, }) end, shopId) end function OpenManagementMenu(shopId) if isNuiOpen then return end ESX.TriggerServerCallback('mercyv-shops:getAccessLevel', function(level, data) if level == 'unowned' then -- Show purchase panel local configShop = Config.Shops[shopId] isNuiOpen = true currentShopId = shopId currentViewMode = 'purchase' SetNuiFocus(true, true) SendNUIMessage({ type = 'openPurchase', shopId = shopId, shopName = configShop.label, price = configShop.price, theme = Config.Theme, }) elseif level == 'owner' then ESX.TriggerServerCallback('mercyv-shops:getPlayerItems', function(playerItems) isNuiOpen = true currentShopId = shopId currentViewMode = 'owner' SetNuiFocus(true, true) SendNUIMessage({ type = 'openOwner', shopId = shopId, shop = data.shop, shelves = data.shelves, storage = data.storage, employees = data.employees, playerItems = playerItems, defaultItems = data.defaultItems, theme = Config.Theme, categories = Config.Categories, }) end) elseif level == 'employee' then isNuiOpen = true currentShopId = shopId currentViewMode = 'employee' SetNuiFocus(true, true) SendNUIMessage({ type = 'openEmployee', shopId = shopId, shop = data.shop, shelves = data.shelves, storage = data.storage, theme = Config.Theme, categories = Config.Categories, }) else TriggerEvent('hex_4_hud:notify', 'Shop', 'Kein Zugriff auf diesen Shop', 'error', 3000) end end, shopId) end function CloseMenu() if not isNuiOpen then return end isNuiOpen = false currentShopId = nil currentViewMode = nil SetNuiFocus(false, false) SendNUIMessage({ type = 'close' }) end -- ===================== -- NUI CALLBACKS -- ===================== RegisterNUICallback('close', function(_, cb) CloseMenu() cb('ok') end) RegisterNUICallback('purchaseShop', function(data, cb) TriggerServerEvent('mercyv-shops:purchaseShop', data.shopId) -- Menu wird vom Server per forceClose oder syncShops geschlossen cb('ok') end) RegisterNUICallback('buyProduct', function(data, cb) TriggerServerEvent('mercyv-shops:buyProduct', currentShopId, data.itemName, data.quantity) cb('ok') end) RegisterNUICallback('addToStorage', function(data, cb) TriggerServerEvent('mercyv-shops:addToStorage', currentShopId, data.itemName, data.itemLabel, data.quantity) cb('ok') end) RegisterNUICallback('moveToShelves', function(data, cb) TriggerServerEvent('mercyv-shops:moveToShelves', currentShopId, data.itemName, data.quantity, data.price) cb('ok') end) RegisterNUICallback('moveToStorage', function(data, cb) TriggerServerEvent('mercyv-shops:moveToStorage', currentShopId, data.itemName, data.quantity) cb('ok') end) RegisterNUICallback('updateShelfPrice', function(data, cb) TriggerServerEvent('mercyv-shops:updateShelfPrice', currentShopId, data.itemName, data.price) cb('ok') end) RegisterNUICallback('removeFromShelves', function(data, cb) TriggerServerEvent('mercyv-shops:removeFromShelves', currentShopId, data.itemName) cb('ok') end) RegisterNUICallback('withdrawVault', function(data, cb) TriggerServerEvent('mercyv-shops:withdrawVault', currentShopId, data.amount) cb('ok') end) RegisterNUICallback('addEmployee', function(data, cb) TriggerServerEvent('mercyv-shops:addEmployee', currentShopId, data.serverId) cb('ok') end) RegisterNUICallback('removeEmployee', function(data, cb) TriggerServerEvent('mercyv-shops:removeEmployee', currentShopId, data.identifier) cb('ok') end) RegisterNUICallback('renameShop', function(data, cb) TriggerServerEvent('mercyv-shops:renameShop', currentShopId, data.name) cb('ok') end) RegisterNUICallback('sellToPlayer', function(data, cb) TriggerServerEvent('mercyv-shops:sellToPlayer', currentShopId, data.serverId, data.price) CloseMenu() cb('ok') end) RegisterNUICallback('sellToServer', function(data, cb) TriggerServerEvent('mercyv-shops:sellToServer', currentShopId) cb('ok') end) RegisterNUICallback('orderItems', function(data, cb) TriggerServerEvent('mercyv-shops:orderItems', currentShopId, data.items) cb('ok') end) RegisterNUICallback('getHistory', function(data, cb) ESX.TriggerServerCallback('mercyv-shops:getSalesHistory', function(history) SendNUIMessage({ type = 'updateHistory', history = history }) end, currentShopId) cb('ok') end) RegisterNUICallback('refreshData', function(_, cb) if not currentShopId then cb('ok'); return end TriggerServerEvent('mercyv-shops:requestRefresh', currentShopId) cb('ok') end) -- ===================== -- SERVER EVENT HANDLERS -- ===================== RegisterNetEvent('mercyv-shops:notify') AddEventHandler('mercyv-shops:notify', function(message, nType) TriggerEvent('hex_4_hud:notify', 'Shop', message, nType or 'info', 3000) end) RegisterNetEvent('mercyv-shops:buyResult') AddEventHandler('mercyv-shops:buyResult', function(success, shopId, lockedCategories) if success and currentShopId == shopId then ESX.TriggerServerCallback('mercyv-shops:getShelfItems', function(shelfItems, serverLocked) SendNUIMessage({ type = 'updateShelves', shelves = shelfItems, lockedCategories = lockedCategories or serverLocked or {}, }) end, shopId) end end) RegisterNetEvent('mercyv-shops:dataRefresh') AddEventHandler('mercyv-shops:dataRefresh', function(shopId) if currentShopId == shopId then TriggerServerEvent('mercyv-shops:requestRefresh', shopId) end end) RegisterNetEvent('mercyv-shops:refreshData') AddEventHandler('mercyv-shops:refreshData', function(data) SendNUIMessage({ type = 'refreshAll', shop = data.shop, shelves = data.shelves, storage = data.storage, employees = data.employees, }) end) RegisterNetEvent('mercyv-shops:forceClose') AddEventHandler('mercyv-shops:forceClose', function() CloseMenu() end)