463 lines
16 KiB
Lua
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)
|