601 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			601 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
eventPrefix = '__PolyZone__:'
 | 
						|
PolyZone = {}
 | 
						|
 | 
						|
local defaultColorWalls = {0, 255, 0}
 | 
						|
local defaultColorOutline = {255, 0, 0}
 | 
						|
local defaultColorGrid = {255, 255, 255}
 | 
						|
 | 
						|
-- Utility functions
 | 
						|
local abs = math.abs
 | 
						|
local function _isLeft(p0, p1, p2)
 | 
						|
  local p0x = p0.x
 | 
						|
  local p0y = p0.y
 | 
						|
  return ((p1.x - p0x) * (p2.y - p0y)) - ((p2.x - p0x) * (p1.y - p0y))
 | 
						|
end
 | 
						|
 | 
						|
local function _wn_inner_loop(p0, p1, p2, wn)
 | 
						|
  local p2y = p2.y
 | 
						|
  if (p0.y <= p2y) then
 | 
						|
    if (p1.y > p2y) then
 | 
						|
      if (_isLeft(p0, p1, p2) > 0) then
 | 
						|
        return wn + 1
 | 
						|
      end
 | 
						|
    end
 | 
						|
  else
 | 
						|
    if (p1.y <= p2y) then
 | 
						|
      if (_isLeft(p0, p1, p2) < 0) then
 | 
						|
        return wn - 1
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  return wn
 | 
						|
end
 | 
						|
 | 
						|
function addBlip(pos)
 | 
						|
  local blip = AddBlipForCoord(pos.x, pos.y, 0.0)
 | 
						|
  SetBlipColour(blip, 7)
 | 
						|
  SetBlipDisplay(blip, 8)
 | 
						|
  SetBlipScale(blip, 1.0)
 | 
						|
  SetBlipAsShortRange(blip, true)
 | 
						|
  return blip
 | 
						|
end
 | 
						|
 | 
						|
function clearTbl(tbl)
 | 
						|
  -- Only works with contiguous (array-like) tables
 | 
						|
  if tbl == nil then return end
 | 
						|
  for i=1, #tbl do
 | 
						|
    tbl[i] = nil
 | 
						|
  end
 | 
						|
  return tbl
 | 
						|
end
 | 
						|
 | 
						|
function copyTbl(tbl)
 | 
						|
  -- Only a shallow copy, and only works with contiguous (array-like) tables
 | 
						|
  if tbl == nil then return end
 | 
						|
  local ret = {}
 | 
						|
  for i=1, #tbl do
 | 
						|
    ret[i] = tbl[i]
 | 
						|
  end
 | 
						|
  return ret
 | 
						|
end
 | 
						|
 | 
						|
-- Winding Number Algorithm - http://geomalgorithms.com/a03-_inclusion.html
 | 
						|
local function _windingNumber(point, poly)
 | 
						|
  local wn = 0 -- winding number counter
 | 
						|
 | 
						|
  -- loop through all edges of the polygon
 | 
						|
  for i = 1, #poly - 1 do
 | 
						|
    wn = _wn_inner_loop(poly[i], poly[i + 1], point, wn)
 | 
						|
  end
 | 
						|
  -- test last point to first point, completing the polygon
 | 
						|
  wn = _wn_inner_loop(poly[#poly], poly[1], point, wn)
 | 
						|
 | 
						|
  -- the point is outside only when this winding number wn===0, otherwise it's inside
 | 
						|
  return wn ~= 0
 | 
						|
end
 | 
						|
 | 
						|
-- Detects intersection between two lines
 | 
						|
local function _isIntersecting(a, b, c, d)
 | 
						|
  -- Store calculations in local variables for performance
 | 
						|
  local ax_minus_cx = a.x - c.x
 | 
						|
  local bx_minus_ax = b.x - a.x
 | 
						|
  local dx_minus_cx = d.x - c.x
 | 
						|
  local ay_minus_cy = a.y - c.y
 | 
						|
  local by_minus_ay = b.y - a.y
 | 
						|
  local dy_minus_cy = d.y - c.y
 | 
						|
  local denominator = ((bx_minus_ax) * (dy_minus_cy)) - ((by_minus_ay) * (dx_minus_cx))
 | 
						|
  local numerator1 = ((ay_minus_cy) * (dx_minus_cx)) - ((ax_minus_cx) * (dy_minus_cy))
 | 
						|
  local numerator2 = ((ay_minus_cy) * (bx_minus_ax)) - ((ax_minus_cx) * (by_minus_ay))
 | 
						|
 | 
						|
  -- Detect coincident lines
 | 
						|
  if denominator == 0 then return numerator1 == 0 and numerator2 == 0 end
 | 
						|
 | 
						|
  local r = numerator1 / denominator
 | 
						|
  local s = numerator2 / denominator
 | 
						|
 | 
						|
  return (r >= 0 and r <= 1) and (s >= 0 and s <= 1)
 | 
						|
end
 | 
						|
 | 
						|
-- https://rosettacode.org/wiki/Shoelace_formula_for_polygonal_area#Lua
 | 
						|
local function _calculatePolygonArea(points)
 | 
						|
  local function det2(i,j)
 | 
						|
    return points[i].x*points[j].y-points[j].x*points[i].y
 | 
						|
  end
 | 
						|
  local sum = #points>2 and det2(#points,1) or 0
 | 
						|
  for i=1,#points-1 do sum = sum + det2(i,i+1)end
 | 
						|
  return abs(0.5 * sum)
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
-- Debug drawing functions
 | 
						|
function _drawWall(p1, p2, minZ, maxZ, r, g, b, a)
 | 
						|
  local bottomLeft = vector3(p1.x, p1.y, minZ)
 | 
						|
  local topLeft = vector3(p1.x, p1.y, maxZ)
 | 
						|
  local bottomRight = vector3(p2.x, p2.y, minZ)
 | 
						|
  local topRight = vector3(p2.x, p2.y, maxZ)
 | 
						|
 | 
						|
  DrawPoly(bottomLeft,topLeft,bottomRight,r,g,b,a)
 | 
						|
  DrawPoly(topLeft,topRight,bottomRight,r,g,b,a)
 | 
						|
  DrawPoly(bottomRight,topRight,topLeft,r,g,b,a)
 | 
						|
  DrawPoly(bottomRight,topLeft,bottomLeft,r,g,b,a)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:TransformPoint(point)
 | 
						|
  -- No point transform necessary for regular PolyZones, unlike zones like Entity Zones, whose points can be rotated and offset
 | 
						|
  return point
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:draw(forceDraw)
 | 
						|
  if not forceDraw and not self.debugPoly and not self.debugGrid then return end
 | 
						|
  
 | 
						|
  local zDrawDist = 45.0
 | 
						|
  local oColor = self.debugColors.outline or defaultColorOutline
 | 
						|
  local oR, oG, oB = oColor[1], oColor[2], oColor[3]
 | 
						|
  local wColor = self.debugColors.walls or defaultColorWalls
 | 
						|
  local wR, wG, wB = wColor[1], wColor[2], wColor[3]
 | 
						|
  local plyPed = PlayerPedId()
 | 
						|
  local plyPos = GetEntityCoords(plyPed)
 | 
						|
  local minZ = self.minZ or plyPos.z - zDrawDist
 | 
						|
  local maxZ = self.maxZ or plyPos.z + zDrawDist
 | 
						|
 | 
						|
  local points = self.points
 | 
						|
  for i=1, #points do
 | 
						|
    local point = self:TransformPoint(points[i])
 | 
						|
    DrawLine(point.x, point.y, minZ, point.x, point.y, maxZ, oR, oG, oB, 164)
 | 
						|
 | 
						|
    if i < #points then
 | 
						|
      local p2 = self:TransformPoint(points[i+1])
 | 
						|
      DrawLine(point.x, point.y, maxZ, p2.x, p2.y, maxZ, oR, oG, oB, 184)
 | 
						|
      _drawWall(point, p2, minZ, maxZ, wR, wG, wB, 48)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  if #points > 2 then
 | 
						|
    local firstPoint = self:TransformPoint(points[1])
 | 
						|
    local lastPoint = self:TransformPoint(points[#points])
 | 
						|
    DrawLine(firstPoint.x, firstPoint.y, maxZ, lastPoint.x, lastPoint.y, maxZ, oR, oG, oB, 184)
 | 
						|
    _drawWall(firstPoint, lastPoint, minZ, maxZ, wR, wG, wB, 48)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone.drawPoly(poly, forceDraw)
 | 
						|
  PolyZone.draw(poly, forceDraw)
 | 
						|
end
 | 
						|
 | 
						|
-- Debug drawing all grid cells that are completly within the polygon
 | 
						|
local function _drawGrid(poly)
 | 
						|
  local minZ = poly.minZ
 | 
						|
  local maxZ = poly.maxZ
 | 
						|
  if not minZ or not maxZ then
 | 
						|
    local plyPed = PlayerPedId()
 | 
						|
    local plyPos = GetEntityCoords(plyPed)
 | 
						|
    minZ = plyPos.z - 46.0
 | 
						|
    maxZ = plyPos.z - 45.0
 | 
						|
  end
 | 
						|
 | 
						|
  local lines = poly.lines
 | 
						|
  local color = poly.debugColors.grid or defaultColorGrid
 | 
						|
  local r, g, b = color[1], color[2], color[3]
 | 
						|
  for i=1, #lines do
 | 
						|
    local line = lines[i]
 | 
						|
    local min = line.min
 | 
						|
    local max = line.max
 | 
						|
    DrawLine(min.x + 0.0, min.y + 0.0, maxZ + 0.0, max.x + 0.0, max.y + 0.0, maxZ + 0.0, r, g, b, 196)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
local function _pointInPoly(point, poly)
 | 
						|
  local x = point.x
 | 
						|
  local y = point.y
 | 
						|
  local min = poly.min
 | 
						|
  local minX = min.x
 | 
						|
  local minY = min.y
 | 
						|
  local max = poly.max
 | 
						|
 | 
						|
  -- Checks if point is within the polygon's bounding box
 | 
						|
  if x < minX or
 | 
						|
     x > max.x or
 | 
						|
     y < minY or
 | 
						|
     y > max.y then
 | 
						|
      return false
 | 
						|
  end
 | 
						|
 | 
						|
  -- Checks if point is within the polygon's height bounds
 | 
						|
  local minZ = poly.minZ
 | 
						|
  local maxZ = poly.maxZ
 | 
						|
  local z = point.z
 | 
						|
  if (minZ and z < minZ) or (maxZ and z > maxZ) then
 | 
						|
    return false
 | 
						|
  end
 | 
						|
 | 
						|
  -- Returns true if the grid cell associated with the point is entirely inside the poly
 | 
						|
  local grid = poly.grid
 | 
						|
  if grid then
 | 
						|
    local gridDivisions = poly.gridDivisions
 | 
						|
    local size = poly.size
 | 
						|
    local gridPosX = x - minX
 | 
						|
    local gridPosY = y - minY
 | 
						|
    local gridCellX = (gridPosX * gridDivisions) // size.x
 | 
						|
    local gridCellY = (gridPosY * gridDivisions) // size.y
 | 
						|
    local gridCellValue = grid[gridCellY + 1][gridCellX + 1]
 | 
						|
    if gridCellValue == nil and poly.lazyGrid then
 | 
						|
      gridCellValue = _isGridCellInsidePoly(gridCellX, gridCellY, poly)
 | 
						|
      grid[gridCellY + 1][gridCellX + 1] = gridCellValue
 | 
						|
    end
 | 
						|
    if gridCellValue then return true end
 | 
						|
  end
 | 
						|
 | 
						|
  return _windingNumber(point, poly.points)
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
-- Grid creation functions
 | 
						|
-- Calculates the points of the rectangle that make up the grid cell at grid position (cellX, cellY)
 | 
						|
local function _calculateGridCellPoints(cellX, cellY, poly)
 | 
						|
  local gridCellWidth = poly.gridCellWidth
 | 
						|
  local gridCellHeight = poly.gridCellHeight
 | 
						|
  local min = poly.min
 | 
						|
  -- min added to initial point, in order to shift the grid cells to the poly's starting position
 | 
						|
  local x = cellX * gridCellWidth + min.x
 | 
						|
  local y = cellY * gridCellHeight + min.y
 | 
						|
  return {
 | 
						|
    vector2(x, y),
 | 
						|
    vector2(x + gridCellWidth, y),
 | 
						|
    vector2(x + gridCellWidth, y + gridCellHeight),
 | 
						|
    vector2(x, y + gridCellHeight),
 | 
						|
    vector2(x, y)
 | 
						|
  }
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
function _isGridCellInsidePoly(cellX, cellY, poly)
 | 
						|
  gridCellPoints = _calculateGridCellPoints(cellX, cellY, poly)
 | 
						|
  local polyPoints = {table.unpack(poly.points)}
 | 
						|
  -- Connect the polygon to its starting point
 | 
						|
  polyPoints[#polyPoints + 1] = polyPoints[1]
 | 
						|
 | 
						|
  -- If none of the points of the grid cell are in the polygon, the grid cell can't be in it
 | 
						|
  local isOnePointInPoly = false
 | 
						|
  for i=1, #gridCellPoints - 1 do
 | 
						|
    local cellPoint = gridCellPoints[i]
 | 
						|
    local x = cellPoint.x
 | 
						|
    local y = cellPoint.y
 | 
						|
    if _windingNumber(cellPoint, poly.points) then
 | 
						|
      isOnePointInPoly = true
 | 
						|
      -- If we are drawing the grid (poly.lines ~= nil), we need to go through all the points,
 | 
						|
      -- and therefore can't break out of the loop early
 | 
						|
      if poly.lines then
 | 
						|
        if not poly.gridXPoints[x] then poly.gridXPoints[x] = {} end
 | 
						|
        if not poly.gridYPoints[y] then poly.gridYPoints[y] = {} end
 | 
						|
        poly.gridXPoints[x][y] = true
 | 
						|
        poly.gridYPoints[y][x] = true
 | 
						|
      else break end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  if isOnePointInPoly == false then
 | 
						|
    return false
 | 
						|
  end
 | 
						|
 | 
						|
  -- If any of the grid cell's lines intersects with any of the polygon's lines
 | 
						|
  -- then the grid cell is not completely within the poly
 | 
						|
  for i=1, #gridCellPoints - 1 do
 | 
						|
    local gridCellP1 = gridCellPoints[i]
 | 
						|
    local gridCellP2 = gridCellPoints[i+1]
 | 
						|
    for j=1, #polyPoints - 1 do
 | 
						|
      if _isIntersecting(gridCellP1, gridCellP2, polyPoints[j], polyPoints[j+1]) then
 | 
						|
        return false
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  return true
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
local function _calculateLinesForDrawingGrid(poly)
 | 
						|
  local lines = {}
 | 
						|
  for x, tbl in pairs(poly.gridXPoints) do
 | 
						|
    local yValues = {}
 | 
						|
    -- Turn dict/set of values into array
 | 
						|
    for y, _ in pairs(tbl) do yValues[#yValues + 1] = y end
 | 
						|
    if #yValues >= 2 then
 | 
						|
      table.sort(yValues)
 | 
						|
      local minY = yValues[1]
 | 
						|
      local lastY = yValues[1]
 | 
						|
      for i=1, #yValues do
 | 
						|
        local y = yValues[i]
 | 
						|
        -- Checks for breaks in the grid. If the distance between the last value and the current one
 | 
						|
        -- is greater than the size of a grid cell, that means the line between them must go outside the polygon.
 | 
						|
        -- Therefore, a line must be created between minY and the lastY, and a new line started at the current y
 | 
						|
        if y - lastY > poly.gridCellHeight + 0.01 then
 | 
						|
          lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, lastY)}
 | 
						|
          minY = y
 | 
						|
        elseif i == #yValues then
 | 
						|
          -- If at the last point, create a line between minY and the last point
 | 
						|
          lines[#lines+1] = {min=vector2(x, minY), max=vector2(x, y)}
 | 
						|
        end
 | 
						|
        lastY = y
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  -- Setting nil to allow the GC to clear it out of memory, since we no longer need this
 | 
						|
  poly.gridXPoints = nil
 | 
						|
 | 
						|
  -- Same as above, but for gridYPoints instead of gridXPoints
 | 
						|
  for y, tbl in pairs(poly.gridYPoints) do
 | 
						|
    local xValues = {}
 | 
						|
    for x, _ in pairs(tbl) do xValues[#xValues + 1] = x end
 | 
						|
    if #xValues >= 2 then
 | 
						|
      table.sort(xValues)
 | 
						|
      local minX = xValues[1]
 | 
						|
      local lastX = xValues[1]
 | 
						|
      for i=1, #xValues do
 | 
						|
        local x = xValues[i]
 | 
						|
        if x - lastX > poly.gridCellWidth + 0.01 then
 | 
						|
          lines[#lines+1] = {min=vector2(minX, y), max=vector2(lastX, y)}
 | 
						|
          minX = x
 | 
						|
        elseif i == #xValues then
 | 
						|
          lines[#lines+1] = {min=vector2(minX, y), max=vector2(x, y)}
 | 
						|
        end
 | 
						|
        lastX = x
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
  poly.gridYPoints = nil
 | 
						|
  return lines
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
-- Calculate for each grid cell whether it is entirely inside the polygon, and store if true
 | 
						|
local function _createGrid(poly, options)
 | 
						|
  poly.gridArea = 0.0
 | 
						|
  poly.gridCellWidth = poly.size.x / poly.gridDivisions
 | 
						|
  poly.gridCellHeight = poly.size.y / poly.gridDivisions
 | 
						|
  Citizen.CreateThread(function()
 | 
						|
    -- Calculate all grid cells that are entirely inside the polygon
 | 
						|
    local isInside = {}
 | 
						|
    local gridCellArea = poly.gridCellWidth * poly.gridCellHeight
 | 
						|
    for y=1, poly.gridDivisions do
 | 
						|
      Citizen.Wait(0)
 | 
						|
      isInside[y] = {}
 | 
						|
      for x=1, poly.gridDivisions do
 | 
						|
        if _isGridCellInsidePoly(x-1, y-1, poly) then
 | 
						|
          poly.gridArea = poly.gridArea + gridCellArea
 | 
						|
          isInside[y][x] = true
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
    poly.grid = isInside
 | 
						|
    poly.gridCoverage = poly.gridArea / poly.area
 | 
						|
    -- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out
 | 
						|
    collectgarbage("collect")
 | 
						|
 | 
						|
    if options.debugGrid then
 | 
						|
      local coverage = string.format("%.2f", poly.gridCoverage * 100)
 | 
						|
      print("[PolyZone] Debug: Grid Coverage at " .. coverage .. "% with " .. poly.gridDivisions
 | 
						|
      .. " divisions. Optimal coverage for memory usage and startup time is 80-90%")
 | 
						|
 | 
						|
      Citizen.CreateThread(function()
 | 
						|
        poly.lines = _calculateLinesForDrawingGrid(poly)
 | 
						|
        -- A lot of memory is used by this pre-calc. Force a gc collect after to clear it out
 | 
						|
        collectgarbage("collect")
 | 
						|
      end)
 | 
						|
    end
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
-- Initialization functions
 | 
						|
local function _calculatePoly(poly, options)
 | 
						|
  if not poly.min or not poly.max or not poly.size or not poly.center or not poly.area then
 | 
						|
    local minX, minY = math.maxinteger, math.maxinteger
 | 
						|
    local maxX, maxY = math.mininteger, math.mininteger
 | 
						|
    for _, p in ipairs(poly.points) do
 | 
						|
      minX = math.min(minX, p.x)
 | 
						|
      minY = math.min(minY, p.y)
 | 
						|
      maxX = math.max(maxX, p.x)
 | 
						|
      maxY = math.max(maxY, p.y)
 | 
						|
    end
 | 
						|
    poly.min = vector2(minX, minY)
 | 
						|
    poly.max = vector2(maxX, maxY)
 | 
						|
    poly.size = poly.max - poly.min
 | 
						|
    poly.center = (poly.max + poly.min) / 2
 | 
						|
    poly.area = _calculatePolygonArea(poly.points)
 | 
						|
  end
 | 
						|
 | 
						|
  poly.boundingRadius = math.sqrt(poly.size.y * poly.size.y + poly.size.x * poly.size.x) / 2
 | 
						|
 | 
						|
  if poly.useGrid and not poly.lazyGrid then
 | 
						|
    if options.debugGrid then
 | 
						|
      poly.gridXPoints = {}
 | 
						|
      poly.gridYPoints = {}
 | 
						|
      poly.lines = {}
 | 
						|
    end
 | 
						|
    _createGrid(poly, options)
 | 
						|
  elseif poly.useGrid then
 | 
						|
    local isInside = {}
 | 
						|
    for y=1, poly.gridDivisions do
 | 
						|
      isInside[y] = {}
 | 
						|
    end
 | 
						|
    poly.grid = isInside
 | 
						|
    poly.gridCellWidth = poly.size.x / poly.gridDivisions
 | 
						|
    poly.gridCellHeight = poly.size.y / poly.gridDivisions
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
 | 
						|
local function _initDebug(poly, options)
 | 
						|
  if options.debugBlip then poly:addDebugBlip() end
 | 
						|
  local debugEnabled = options.debugPoly or options.debugGrid
 | 
						|
  if not debugEnabled then
 | 
						|
    return
 | 
						|
  end
 | 
						|
 | 
						|
  Citizen.CreateThread(function()
 | 
						|
    while not poly.destroyed do
 | 
						|
      poly:draw(false)
 | 
						|
      if options.debugGrid and poly.lines then
 | 
						|
        _drawGrid(poly)
 | 
						|
      end
 | 
						|
      Citizen.Wait(0)
 | 
						|
    end
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:new(points, options)
 | 
						|
  if not points then
 | 
						|
    print("[PolyZone] Error: Passed nil points table to PolyZone:Create() {name=" .. options.name .. "}")
 | 
						|
    return
 | 
						|
  end
 | 
						|
  if #points < 3 then
 | 
						|
    print("[PolyZone] Warning: Passed points table with less than 3 points to PolyZone:Create() {name=" .. options.name .. "}")
 | 
						|
  end
 | 
						|
 | 
						|
  options = options or {}
 | 
						|
  local useGrid = options.useGrid
 | 
						|
  if useGrid == nil then useGrid = true end
 | 
						|
  local lazyGrid = options.lazyGrid
 | 
						|
  if lazyGrid == nil then lazyGrid = true end
 | 
						|
  local poly = {
 | 
						|
    name = tostring(options.name) or nil,
 | 
						|
    points = points,
 | 
						|
    center = options.center,
 | 
						|
    size = options.size,
 | 
						|
    max = options.max,
 | 
						|
    min = options.min,
 | 
						|
    area = options.area,
 | 
						|
    minZ = tonumber(options.minZ) or nil,
 | 
						|
    maxZ = tonumber(options.maxZ) or nil,
 | 
						|
    useGrid = useGrid,
 | 
						|
    lazyGrid = lazyGrid,
 | 
						|
    gridDivisions = tonumber(options.gridDivisions) or 30,
 | 
						|
    debugColors = options.debugColors or {},
 | 
						|
    debugPoly = options.debugPoly or false,
 | 
						|
    debugGrid = options.debugGrid or false,
 | 
						|
    data = options.data or {},
 | 
						|
    isPolyZone = true,
 | 
						|
  }
 | 
						|
  if poly.debugGrid then poly.lazyGrid = false end
 | 
						|
  _calculatePoly(poly, options)
 | 
						|
  setmetatable(poly, self)
 | 
						|
  self.__index = self
 | 
						|
  return poly
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:Create(points, options)
 | 
						|
  local poly = PolyZone:new(points, options)
 | 
						|
  _initDebug(poly, options)
 | 
						|
  return poly
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:isPointInside(point)
 | 
						|
  if self.destroyed then
 | 
						|
    print("[PolyZone] Warning: Called isPointInside on destroyed zone {name=" .. self.name .. "}")
 | 
						|
    return false
 | 
						|
  end
 | 
						|
 | 
						|
  return _pointInPoly(point, self)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:destroy()
 | 
						|
  self.destroyed = true
 | 
						|
  if self.debugPoly or self.debugGrid then
 | 
						|
    print("[PolyZone] Debug: Destroying zone {name=" .. self.name .. "}")
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
-- Helper functions
 | 
						|
function PolyZone.getPlayerPosition()
 | 
						|
  return GetEntityCoords(PlayerPedId())
 | 
						|
end
 | 
						|
 | 
						|
HeadBone = 0x796e;
 | 
						|
function PolyZone.getPlayerHeadPosition()
 | 
						|
  return GetPedBoneCoords(PlayerPedId(), HeadBone);
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone.ensureMetatable(zone)
 | 
						|
  if zone.isComboZone then
 | 
						|
    setmetatable(zone, ComboZone)
 | 
						|
  elseif zone.isEntityZone then
 | 
						|
    setmetatable(zone, EntityZone)
 | 
						|
  elseif zone.isBoxZone then
 | 
						|
    setmetatable(zone, BoxZone)
 | 
						|
  elseif zone.isCircleZone then
 | 
						|
    setmetatable(zone, CircleZone)
 | 
						|
  elseif zone.isPolyZone then
 | 
						|
    setmetatable(zone, PolyZone)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone: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 = false
 | 
						|
    while not self.destroyed do
 | 
						|
      if not self.paused then
 | 
						|
        local point = getPointCb()
 | 
						|
        local newIsInside = self:isPointInside(point)
 | 
						|
        if newIsInside ~= isInside then
 | 
						|
          onPointInOutCb(newIsInside, point)
 | 
						|
          isInside = newIsInside
 | 
						|
        end
 | 
						|
      end
 | 
						|
      Citizen.Wait(_waitInMS)
 | 
						|
    end
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:onPlayerInOut(onPointInOutCb, waitInMS)
 | 
						|
  self:onPointInOut(PolyZone.getPlayerPosition, onPointInOutCb, waitInMS)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:addEvent(eventName)
 | 
						|
  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()) then
 | 
						|
      TriggerEvent(eventName, ...)
 | 
						|
    end
 | 
						|
  end)
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:removeEvent(eventName)
 | 
						|
  if self.events and self.events[eventName] then
 | 
						|
    RemoveEventHandler(self.events[eventName])
 | 
						|
    self.events[eventName] = nil
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:addDebugBlip()
 | 
						|
  return addBlip(self.center or self:getBoundingBoxCenter())
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:setPaused(paused)
 | 
						|
  self.paused = paused
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:isPaused()
 | 
						|
  return self.paused
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:getBoundingBoxMin()
 | 
						|
  return self.min
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:getBoundingBoxMax()
 | 
						|
  return self.max
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:getBoundingBoxSize()
 | 
						|
  return self.size
 | 
						|
end
 | 
						|
 | 
						|
function PolyZone:getBoundingBoxCenter()
 | 
						|
  return self.center
 | 
						|
end
 |