369 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| local mapMinX, mapMinY, mapMaxX, mapMaxY = -3700, -4400, 4500, 8000
 | |
| local xDivisions = 34
 | |
| local yDivisions = 50
 | |
| local xDelta = (mapMaxX - mapMinX) / xDivisions
 | |
| local yDelta = (mapMaxY - mapMinY) / yDivisions
 | |
| 
 | |
| ComboZone = {}
 | |
| 
 | |
| -- Finds all values in tblA that are not in tblB, using the "id" property
 | |
| local function tblDifference(tblA, tblB)
 | |
|   local diff
 | |
|   for _, a in ipairs(tblA) do
 | |
|     local found = false
 | |
|     for _, b in ipairs(tblB) do
 | |
|       if b.id == a.id then
 | |
|         found = true
 | |
|         break
 | |
|       end
 | |
|     end
 | |
|     if not found then
 | |
|       diff = diff or {}
 | |
|       diff[#diff+1] = a
 | |
|     end
 | |
|   end
 | |
|   return diff
 | |
| end
 | |
| 
 | |
| local function _differenceBetweenInsideZones(insideZones, newInsideZones)
 | |
|   local insideZonesCount, newInsideZonesCount = #insideZones, #newInsideZones
 | |
|   if insideZonesCount == 0 and newInsideZonesCount == 0 then
 | |
|     -- No zones to check
 | |
|     return false, nil, nil
 | |
|   elseif insideZonesCount == 0 and newInsideZonesCount > 0 then
 | |
|     -- Was in no zones last check, but in 1 or more zones now (just entered all zones in newInsideZones)
 | |
|     return true, copyTbl(newInsideZones), nil
 | |
|   elseif insideZonesCount > 0 and newInsideZonesCount == 0 then
 | |
|     -- Was in 1 or more zones last check, but in no zones now (just left all zones in insideZones)
 | |
|     return true, nil, copyTbl(insideZones)
 | |
|   end
 | |
| 
 | |
|   -- Check for zones that were in insideZones, but are not in newInsideZones (zones the player just left)
 | |
|   local leftZones = tblDifference(insideZones, newInsideZones)
 | |
|   -- Check for zones that are in newInsideZones, but were not in insideZones (zones the player just entered)
 | |
|   local enteredZones = tblDifference(newInsideZones, insideZones)
 | |
| 
 | |
|   local isDifferent = enteredZones ~= nil or leftZones ~= nil
 | |
|   return isDifferent, enteredZones, leftZones
 | |
| end
 | |
| 
 | |
| local function _getZoneBounds(zone)
 | |
|   local center = zone.center
 | |
|   local radius = zone.radius or zone.boundingRadius
 | |
|   local minY = (center.y - radius - mapMinY) // yDelta
 | |
|   local maxY = (center.y + radius - mapMinY) // yDelta
 | |
|   local minX = (center.x - radius - mapMinX) // xDelta
 | |
|   local maxX = (center.x + radius - mapMinX) // xDelta
 | |
|   return minY, maxY, minX, maxX
 | |
| end
 | |
| 
 | |
| local function _removeZoneByFunction(predicateFn, zones)
 | |
|   if predicateFn == nil or zones == nil or #zones == 0 then return end
 | |
| 
 | |
|   for i=1, #zones do
 | |
|     local possibleZone = zones[i]
 | |
|     if possibleZone and predicateFn(possibleZone) then
 | |
|       table.remove(zones, i)
 | |
|       return possibleZone
 | |
|     end
 | |
|   end
 | |
|   return nil
 | |
| end
 | |
| 
 | |
| local function _addZoneToGrid(grid, zone)
 | |
|   local minY, maxY, minX, maxX = _getZoneBounds(zone)
 | |
|   for y=minY, maxY do
 | |
|     local row = grid[y] or {}
 | |
|     for x=minX, maxX do
 | |
|       local cell = row[x] or {}
 | |
|       cell[#cell+1] = zone
 | |
|       row[x] = cell
 | |
|     end
 | |
|     grid[y] = row
 | |
|   end
 | |
| end
 | |
| 
 | |
| local function _getGridCell(pos)
 | |
|   local x = (pos.x - mapMinX) // xDelta
 | |
|   local y = (pos.y - mapMinY) // yDelta
 | |
|   return x, y
 | |
| end
 | |
| 
 | |
| 
 | |
| function ComboZone:draw(forceDraw)
 | |
|   local zones = self.zones
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone and not zone.destroyed then
 | |
|       zone:draw(forceDraw)
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| 
 | |
| local function _initDebug(zone, options)
 | |
|   if options.debugBlip then zone:addDebugBlip() end
 | |
|   if not options.debugPoly then
 | |
|     return
 | |
|   end
 | |
| 
 | |
|   Citizen.CreateThread(function()
 | |
|     while not zone.destroyed do
 | |
|       zone:draw(false)
 | |
|       Citizen.Wait(0)
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function ComboZone:new(zones, options)
 | |
|   options = options or {}
 | |
|   local useGrid = options.useGrid
 | |
|   if useGrid == nil then useGrid = true end
 | |
| 
 | |
|   local grid = {}
 | |
|   -- Add a unique id for each zone in the ComboZone and add to grid cache
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone then
 | |
|       zone.id = i
 | |
|     end
 | |
|     if useGrid then _addZoneToGrid(grid, zone) end
 | |
|   end
 | |
| 
 | |
|   local zone = {
 | |
|     name = tostring(options.name) or nil,
 | |
|     zones = zones,
 | |
|     useGrid = useGrid,
 | |
|     grid = grid,
 | |
|     debugPoly = options.debugPoly or false,
 | |
|     data = options.data or {},
 | |
|     isComboZone = true,
 | |
|   }
 | |
|   setmetatable(zone, self)
 | |
|   self.__index = self
 | |
|   return zone
 | |
| end
 | |
| 
 | |
| function ComboZone:Create(zones, options)
 | |
|   local zone = ComboZone:new(zones, options)
 | |
|   _initDebug(zone, options)
 | |
|   AddEventHandler("polyzone:pzcomboinfo", function ()
 | |
|       zone:printInfo()
 | |
|   end)
 | |
|   return zone
 | |
| end
 | |
| 
 | |
| function ComboZone:getZones(point)
 | |
|   if not self.useGrid then
 | |
|     return self.zones
 | |
|   end
 | |
| 
 | |
|   local grid = self.grid
 | |
|   local x, y = _getGridCell(point)
 | |
|   local row = grid[y]
 | |
|   if row == nil or row[x] == nil then
 | |
|     return nil
 | |
|   end
 | |
|   return row[x]
 | |
| end
 | |
| 
 | |
| function ComboZone:AddZone(zone)
 | |
|   local zones = self.zones
 | |
|   local newIndex = #zones+1
 | |
|   zone.id = newIndex
 | |
|   zones[newIndex] = zone
 | |
|   if self.useGrid then
 | |
|     _addZoneToGrid(self.grid, zone)
 | |
|   end
 | |
|   if self.debugBlip then zone:addDebugBlip() end
 | |
| end
 | |
| 
 | |
| function ComboZone:RemoveZone(nameOrFn)
 | |
|   local predicateFn = nameOrFn
 | |
|   if type(nameOrFn) == "string" then
 | |
|     -- Create on the fly predicate function if nameOrFn is a string (zone name)
 | |
|     predicateFn = function (zone) return zone.name == nameOrFn end
 | |
|   elseif type(nameOrFn) ~= "function" then
 | |
|     return nil
 | |
|   end
 | |
| 
 | |
|   -- Remove from zones table
 | |
|   local zone = _removeZoneByFunction(predicateFn, self.zones)
 | |
|   if not zone then return nil end
 | |
| 
 | |
|   -- Remove from grid cache
 | |
|   local grid = self.grid
 | |
|   local minY, maxY, minX, maxX = _getZoneBounds(zone)
 | |
|   for y=minY, maxY do
 | |
|     local row = grid[y]
 | |
|     if row then
 | |
|       for x=minX, maxX do
 | |
|         _removeZoneByFunction(predicateFn, row[x])
 | |
|       end
 | |
|     end
 | |
|   end
 | |
|   return zone
 | |
| end
 | |
| 
 | |
| function ComboZone:isPointInside(point, zoneName)
 | |
|   if self.destroyed then
 | |
|     print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
 | |
|     return false, {}
 | |
|   end
 | |
| 
 | |
|   local zones = self:getZones(point)
 | |
|   if not zones or #zones == 0 then return false end
 | |
| 
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone and (zoneName == nil or zoneName == zone.name) and zone:isPointInside(point) then
 | |
|       return true, zone
 | |
|     end
 | |
|   end
 | |
|   return false, nil
 | |
| end
 | |
| 
 | |
| function ComboZone:isPointInsideExhaustive(point, insideZones)
 | |
|   if self.destroyed then
 | |
|     print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
 | |
|     return false, {}
 | |
|   end
 | |
| 
 | |
|   if insideZones ~= nil then
 | |
|     insideZones = clearTbl(insideZones)
 | |
|   else
 | |
|     insideZones = {}
 | |
|   end
 | |
|   local zones = self:getZones(point)
 | |
|   if not zones or #zones == 0 then return false, insideZones end
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone and zone:isPointInside(point) then
 | |
|       insideZones[#insideZones+1] = zone
 | |
|     end
 | |
|   end
 | |
|   return #insideZones > 0, insideZones
 | |
| end
 | |
| 
 | |
| function ComboZone:destroy()
 | |
|   PolyZone.destroy(self)
 | |
|   local zones = self.zones
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone and not zone.destroyed then
 | |
|       zone:destroy()
 | |
|     end
 | |
|   end
 | |
| end
 | |
| 
 | |
| function ComboZone:onPointInOut(getPointCb, onPointInOutCb, waitInMS)
 | |
|   -- Localize the waitInMS value for performance reasons (default of 500 ms)
 | |
|   local _waitInMS = 500
 | |
|   if waitInMS ~= nil then _waitInMS = waitInMS end
 | |
| 
 | |
|   Citizen.CreateThread(function()
 | |
|     local isInside = nil
 | |
|     local insideZone = nil
 | |
|     while not self.destroyed do
 | |
|       if not self.paused then
 | |
|         local point = getPointCb()
 | |
|         local newIsInside, newInsideZone = self:isPointInside(point)
 | |
|         if newIsInside ~= isInside then
 | |
|           onPointInOutCb(newIsInside, point, newInsideZone or insideZone)
 | |
|           isInside = newIsInside
 | |
|           insideZone = newInsideZone
 | |
|         end
 | |
|       end
 | |
|       Citizen.Wait(_waitInMS)
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function ComboZone:onPointInOutExhaustive(getPointCb, onPointInOutCb, waitInMS)
 | |
|   -- Localize the waitInMS value for performance reasons (default of 500 ms)
 | |
|   local _waitInMS = 500
 | |
|   if waitInMS ~= nil then _waitInMS = waitInMS end
 | |
| 
 | |
|   Citizen.CreateThread(function()
 | |
|     local isInside, insideZones = nil, {}
 | |
|     local newIsInside, newInsideZones = nil, {}
 | |
|     while not self.destroyed do
 | |
|       if not self.paused then
 | |
|         local point = getPointCb()
 | |
|         newIsInside, newInsideZones = self:isPointInsideExhaustive(point, newInsideZones)
 | |
|         local isDifferent, enteredZones, leftZones = _differenceBetweenInsideZones(insideZones, newInsideZones)
 | |
|         if newIsInside ~= isInside or isDifferent then
 | |
|           isInside = newIsInside
 | |
|           insideZones = copyTbl(newInsideZones)
 | |
|           onPointInOutCb(isInside, point, insideZones, enteredZones, leftZones)
 | |
|         end
 | |
|       end
 | |
|       Citizen.Wait(_waitInMS)
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function ComboZone:onPlayerInOut(onPointInOutCb, waitInMS)
 | |
|   self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS)
 | |
| end
 | |
| 
 | |
| function ComboZone:onPlayerInOutExhaustive(onPointInOutCb, waitInMS)
 | |
|   self:onPointInOutExhaustive(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS)
 | |
| end
 | |
| 
 | |
| function ComboZone:addEvent(eventName, zoneName)
 | |
|   if self.events == nil then self.events = {} end
 | |
|   local internalEventName = eventPrefix .. eventName
 | |
|   RegisterNetEvent(internalEventName)
 | |
|   self.events[eventName] = AddEventHandler(internalEventName, function (...)
 | |
|     if self:isPointInside(PolyZone.getPlayerPosition(), zoneName) then
 | |
|       TriggerEvent(eventName, ...)
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function ComboZone:removeEvent(name)
 | |
|   PolyZone.removeEvent(self, name)
 | |
| end
 | |
| 
 | |
| function ComboZone:addDebugBlip()
 | |
|   self.debugBlip = true
 | |
|   local zones = self.zones
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone then zone:addDebugBlip() end
 | |
|   end
 | |
| end
 | |
| 
 | |
| function ComboZone:printInfo()
 | |
|   local zones = self.zones
 | |
|   local polyCount, boxCount, circleCount, entityCount, comboCount = 0, 0, 0, 0, 0
 | |
|   for i=1, #zones do
 | |
|     local zone = zones[i]
 | |
|     if zone then
 | |
|       if zone.isEntityZone then entityCount = entityCount + 1
 | |
|       elseif zone.isCircleZone then circleCount = circleCount + 1
 | |
|       elseif zone.isComboZone then comboCount = comboCount + 1
 | |
|       elseif zone.isBoxZone then boxCount = boxCount + 1
 | |
|       elseif zone.isPolyZone then polyCount = polyCount + 1 end
 | |
|     end
 | |
|   end
 | |
|   local name = self.name ~= nil and ("\"" .. self.name .. "\"") or nil
 | |
|   print("-----------------------------------------------------")
 | |
|   print("[PolyZone] Info for ComboZone { name = " .. tostring(name) .. " }:")
 | |
|   print("[PolyZone]   Total zones: " .. #zones)
 | |
|   if boxCount > 0 then print("[PolyZone]   BoxZones: " .. boxCount) end
 | |
|   if circleCount > 0 then print("[PolyZone]   CircleZones: " .. circleCount) end
 | |
|   if polyCount > 0 then print("[PolyZone]   PolyZones: " .. polyCount) end
 | |
|   if entityCount > 0 then print("[PolyZone]   EntityZones: " .. entityCount) end
 | |
|   if comboCount > 0 then print("[PolyZone]   ComboZones: " .. comboCount) end
 | |
|   print("-----------------------------------------------------")
 | |
| end
 | |
| 
 | |
| function ComboZone:setPaused(paused)
 | |
|   self.paused = paused
 | |
| end
 | |
| 
 | |
| function ComboZone:isPaused()
 | |
|   return self.paused
 | |
| end
 | 
