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

463 lines
16 KiB
Lua

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)