├── .gitignore ├── fxmanifest.lua ├── LICENSE ├── server └── creation.lua ├── client ├── utils.lua ├── creation.lua └── client.lua └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | lasers.txt 2 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | games {'gta5'} 2 | 3 | fx_version 'cerulean' 4 | 5 | description 'Create moving lasers in FiveM!' 6 | version '1.0.0' 7 | 8 | client_scripts { 9 | 'client/client.lua', 10 | } 11 | 12 | local creationEnabled = true 13 | if creationEnabled then 14 | client_scripts { 15 | 'client/utils.lua', 16 | 'client/creation.lua', 17 | } 18 | server_script 'server/creation.lua' 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Afrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/creation.lua: -------------------------------------------------------------------------------- 1 | function round(num, numDecimalPlaces) 2 | return tonumber(string.format("%.".. numDecimalPlaces .. "f", num)) 3 | end 4 | 5 | function roundVec(vec, numDecimalPlaces) 6 | return vector3(round(vec.x, numDecimalPlaces), round(vec.y, numDecimalPlaces), round(vec.z, numDecimalPlaces)) 7 | end 8 | 9 | function printoutHeader(name) 10 | return "-- Name: " .. (name or "") .. " | " .. os.date("!%Y-%m-%dT%H:%M:%SZ\n") 11 | end 12 | 13 | RegisterNetEvent("mka-lasers:save") 14 | AddEventHandler("mka-lasers:save", function(laser) 15 | local resname = GetCurrentResourceName() 16 | local txt = LoadResourceFile(resname, "lasers.txt") or "" 17 | local newTxt = txt .. parseLaser(laser) 18 | SaveResourceFile(resname, "lasers.txt", newTxt, -1) 19 | end) 20 | 21 | function parseLaser(laser) 22 | local out = printoutHeader(laser.name) 23 | out = out .. "Laser.new(\n" 24 | -- Origin point 25 | if #laser.originPoints == 1 then 26 | out = out .. " " .. roundVec(laser.originPoints[1], 3) .. ",\n" 27 | else 28 | out = out .. " {" 29 | for i, originPoint in ipairs(laser.originPoints) do 30 | out = out .. roundVec(originPoint, 3) 31 | if i < #laser.originPoints then out = out .. ", " end 32 | end 33 | out = out .. "},\n" 34 | end 35 | -- Target points 36 | out = out .. " {" 37 | for i, targetPoint in ipairs(laser.targetPoints) do 38 | out = out .. roundVec(targetPoint, 3) 39 | if i < #laser.targetPoints then out = out .. ", " end 40 | end 41 | out = out .. "},\n" 42 | -- Options 43 | out = out .. " {" 44 | out = out .. string.format("travelTimeBetweenTargets = {%s, %s}, ", tostring(round(laser.travelTimeBetweenTargets[1], 3)), tostring(round(laser.travelTimeBetweenTargets[2], 3))) 45 | out = out .. string.format("waitTimeAtTargets = {%s, %s}", tostring(round(laser.waitTimeAtTargets[1], 3)), tostring(round(laser.waitTimeAtTargets[2], 3))) 46 | if #laser.originPoints == 1 then 47 | out = out .. ", randomTargetSelection = " .. tostring(laser.randomTargetSelection) 48 | end 49 | if laser.name then 50 | out = out .. string.format(', name = "%s"', tostring(laser.name)) 51 | end 52 | out = out .. "}\n" 53 | out = out .. ")\n\n" 54 | return out 55 | end 56 | -------------------------------------------------------------------------------- /client/utils.lua: -------------------------------------------------------------------------------- 1 | local pi, sin, cos, abs = math.pi, math.sin, math.cos, math.abs 2 | local function RotationToDirection(rotation) 3 | local piDivBy180 = pi / 180 4 | local adjustedRotation = vector3( 5 | piDivBy180 * rotation.x, 6 | piDivBy180 * rotation.y, 7 | piDivBy180 * rotation.z 8 | ) 9 | local direction = vector3( 10 | -sin(adjustedRotation.z) * abs(cos(adjustedRotation.x)), 11 | cos(adjustedRotation.z) * abs(cos(adjustedRotation.x)), 12 | sin(adjustedRotation.x) 13 | ) 14 | return direction 15 | end 16 | 17 | function RayCastGamePlayCamera(distance) 18 | local cameraRotation = GetGameplayCamRot() 19 | local cameraCoord = GetGameplayCamCoord() 20 | --local right, direction, up, pos = GetCamMatrix(GetRenderingCam()) 21 | --local cameraCoord = pos 22 | local direction = RotationToDirection(cameraRotation) 23 | local destination = vector3( 24 | cameraCoord.x + direction.x * distance, 25 | cameraCoord.y + direction.y * distance, 26 | cameraCoord.z + direction.z * distance 27 | ) 28 | local ray = StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, 29 | destination.x, destination.y, destination.z, 1, -1, 0) 30 | local rayHandle, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(ray) 31 | return hit, endCoords, entityHit, surfaceNormal 32 | end 33 | 34 | -- GetUserInput function inspired by vMenu (https://github.com/TomGrobbe/vMenu/blob/master/vMenu/CommonFunctions.cs) 35 | function GetUserInput(windowTitle, defaultText, maxInputLength) 36 | blockinput = true 37 | -- Create the window title string. 38 | local resourceName = string.upper(GetCurrentResourceName()) 39 | local textEntry = resourceName .. "_WINDOW_TITLE" 40 | if windowTitle == nil then 41 | windowTitle = "Enter:" 42 | end 43 | AddTextEntry(textEntry, windowTitle) 44 | 45 | -- Display the input box. 46 | DisplayOnscreenKeyboard(1, textEntry, "", defaultText or "", "", "", "", maxInputLength or 30) 47 | Wait(0) 48 | -- Wait for a result. 49 | while true do 50 | local keyboardStatus = UpdateOnscreenKeyboard(); 51 | if keyboardStatus == 3 then -- not displaying input field anymore somehow 52 | blockinput = false 53 | return nil 54 | elseif keyboardStatus == 2 then -- cancelled 55 | blockinput = false 56 | return nil 57 | elseif keyboardStatus == 1 then -- finished editing 58 | blockinput = false 59 | return GetOnscreenKeyboardResult() 60 | else 61 | Wait(0) 62 | end 63 | end 64 | end 65 | 66 | function randomTargetSelectionInput() 67 | local randomTargetSelection = GetUserInput("Should the laser randomly select it's next target point? (Y/n)", "", 1) 68 | if randomTargetSelection == nil then return nil end 69 | if randomTargetSelection == "" or string.lower(randomTargetSelection) == "y" then return true end 70 | if string.lower(randomTargetSelection) == "n" then return false end 71 | return randomTargetSelection 72 | end 73 | 74 | function DrawSphere(pos, radius, r, g, b, a) 75 | DrawMarker(28, pos.x, pos.y, pos.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, radius, radius, radius, r, g, b, a, false, false, 2, nil, nil, false) 76 | end 77 | -------------------------------------------------------------------------------- /client/creation.lua: -------------------------------------------------------------------------------- 1 | local maxDistance = 50.0 2 | local laserPointRadius = 0.1 3 | local originPoints, targetPoints 4 | local inOriginMode = false 5 | 6 | local creationEnabled = false 7 | RegisterCommand("lasers", function(src, args) 8 | local command = args[1] 9 | if command == "start" and not creationEnabled then 10 | creationEnabled = true 11 | inOriginMode = true 12 | startCreation() 13 | elseif command == "end" and creationEnabled then 14 | creationEnabled = false 15 | elseif command == "save" and creationEnabled then 16 | if not originPoints or not targetPoints then return end 17 | local name = GetUserInput("Enter name of laser:", "", 30) 18 | if name == nil then return end 19 | local laser = { 20 | name=name, 21 | originPoints=originPoints, 22 | targetPoints=targetPoints, 23 | travelTimeBetweenTargets={1.0, 1.0}, 24 | waitTimeAtTargets={0.0, 0.0}, 25 | randomTargetSelection=true 26 | } 27 | TriggerServerEvent("mka-lasers:save", laser) 28 | creationEnabled = false 29 | end 30 | end) 31 | Citizen.CreateThread(function() 32 | TriggerEvent('chat:addSuggestion', '/lasers', '', { 33 | {name="command", help="{start, end, save} (required)"}, 34 | }) 35 | end) 36 | 37 | function startCreation() 38 | if not creationEnabled then return end 39 | originPoints, targetPoints = {}, {} 40 | Citizen.CreateThread(function() 41 | while creationEnabled do 42 | if inOriginMode == true and IsControlJustReleased(0, 73) then -- X key to switch to target mode 43 | inOriginMode = false 44 | end 45 | drawPoints() 46 | drawLines() 47 | if inOriginMode then 48 | handleLaserOriginPoint() 49 | else 50 | handleLaserTargetPoints() 51 | end 52 | Wait(0) 53 | end 54 | end) 55 | end 56 | 57 | function handleLaserOriginPoint() 58 | local point = handlePoint(0, 255, 0, 255) 59 | if point and originPoints then 60 | originPoints[#originPoints+1] = point 61 | print("Add point to laser originPoints:", point) 62 | end 63 | end 64 | 65 | function handleLaserTargetPoints() 66 | local point = handlePoint(255, 0, 0, 255) 67 | if point then 68 | targetPoints[#targetPoints+1] = point 69 | print("Add point to laser targetPoints:", point) 70 | end 71 | end 72 | 73 | function handlePoint(r, g, b, a) 74 | local hit, pos, _, _ = RayCastGamePlayCamera(maxDistance) 75 | if hit then 76 | DrawSphere(pos, laserPointRadius, r, g, b, a) 77 | if IsControlJustReleased(0, 51) then 78 | return pos 79 | end 80 | end 81 | end 82 | 83 | function drawPoints() 84 | if not originPoints then return end 85 | for _, originPoint in ipairs(originPoints) do 86 | DrawSphere(originPoint, laserPointRadius, 0, 255, 0, 255) 87 | end 88 | if not targetPoints then return end 89 | for _, targetPoint in ipairs(targetPoints) do 90 | DrawSphere(targetPoint, laserPointRadius, 255, 0, 0, 255) 91 | end 92 | end 93 | 94 | function drawLines() 95 | if not originPoints or #originPoints == 0 or not targetPoints or #targetPoints == 0 then return end 96 | if #originPoints == 1 then 97 | for _, targetPoint in ipairs(targetPoints) do 98 | DrawLine(originPoints[1], targetPoint, 255, 0, 0, 255) 99 | end 100 | else 101 | for i=1, #originPoints do 102 | if i <= #targetPoints then 103 | DrawLine(originPoints[i], targetPoints[i], 255, 0, 0, 255) 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mka-lasers 2 | Create moving lasers in FiveM! 3 | 4 | Example of lasers in bank vault 5 | 6 | ## Creation 7 | Creating new lasers is simple. The `/lasers` command takes three possible sub-commands (start, end, save). 8 | 9 | To start creating a new laser, use the command `/lasers start`. A green sphere will appear where you are looking in-game. This is the laser's "origin point" (where the laser will start). You can press E to select that point. You can have multiple origin points, which enables situations where you have a laser moving back and forth between multiple origin-target point pairs. To place more than one origin point just keep pressing E to add more points. 10 | 11 | To switch to target point selection mode, press the X key, and the sphere will turn red. You can press E to place a point. Just like origin points, you can place as many target points as you want, however if you have multiple origin points, you must have the same number of target points. These points are the "targets" that your laser will point to. The laser can either follow these points in order, or randomly (only available if you have a single origin point), so make sure to place them in the order you want! 12 | 13 | Once you are done selecting the target points, use `/lasers save` to save the created laser. You will be asked to input a name, and then the generated code for the newly created laser will be in the "lasers.txt" file in the resource's folder. 14 | 15 | ## How to Use 16 | To use your newly created laser, you need to do two things. 17 | 18 | 1. Import mka-lasers into the resource you want to use it in. You can do this by adding the following to your resource's __resource.lua or fxmanifest.lua file. 19 | ```lua 20 | client_scripts { 21 | '@mka-lasers/client/client.lua', 22 | 'your_scripts_client.lua', 23 | } 24 | ``` 25 | 2. Paste the generated code for your laser from the "lasers.txt" file anywhere in your resource. 26 | 3. Call setActive(true) on the laser(s) to turn them on. 27 | 28 | 29 | ## Laser Options 30 | 31 | | Property | Type |       Default       | Required | Description | 32 | |--------------------------|---------|---------------------------------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 33 | | name | String | "" | false | Name of the laser | 34 | | travelTimeBetweenTargets | Table | {1.0, 1.0} | false | The amount of time in seconds for the laser to travel from one target point to the next. This is a table of two values representing the minimum and maximum time, which is randomly selected between. If you don't want a random value, simply put the same number for both. | 35 | | waitTimeAtTargets | Table | {0.0, 0.0} | false | The amount of time in seconds the laser will wait once it reaches a target point. This is a table of two values representing the minimum and maximum time, which is randomly selected between. If you don't want a random value, simply put the same number for both. | 36 | | randomTargetSelection | Boolean | true | false | Whether the laser randomly selects the next target point. If this is false, the next point in the original order will be selected. | 37 | | maxDistance | Float | 20.0 | false | Maximum distance of the laser. | 38 | | color | Table | {255, 0, 0, 255} | false | Color of the laser in rgba format (red, blue, green, alpha). This has to be a table of four integers representing each of the four colors in rgba. | 39 | 40 | ## onPlayerHit 41 | onPlayerHit is a function on a laser which will call the given callback function anytime the laser goes from not hitting a player to hitting them, and vice-versa. 42 | ### Using onPlayerHit 43 | ```lua 44 | laser.onPlayerHit(function(playerBeingHit, hitPos) 45 | if playerBeingHit then 46 | -- Laser just hit the player 47 | else 48 | -- Laser just stopped hitting the player 49 | -- hitPos will just be a zero vector here 50 | end 51 | end) 52 | 53 | -- You can clear out onPlayerHit by just calling clearOnPlayerHit() 54 | laser.clearOnPlayerHit() 55 | ``` 56 | 57 | 58 | ## Other Laser Functions 59 | Laser's have a few functions that can be useful for manipulating the laser after you create it. They are as follows: 60 | 61 | `getActive()` - Returns the active state of the laser (if it's on or off) \ 62 | `setActive(bool)` - Sets the active state of the laser \ 63 | `getVisible()` - Returns the visibility of the laser \ 64 | `setVisible(bool)` - Sets the visibility of the laser \ 65 | `getMoving()` - Returns whether the laser is moving or not \ 66 | `setMoving(bool)` - Sets whether the laser is moving or not \ 67 | `getColor()` - Returns the color of the laser as red, green, blue, and alpha (rgba) \ 68 | `setColor(int, int, int, int)` - Sets the color of the laser using red, green, blue, and alpha (rgba) 69 | 70 | 71 | -------------------------------------------------------------------------------- /client/client.lua: -------------------------------------------------------------------------------- 1 | Laser = {} 2 | 3 | local ShapeTestRay = StartShapeTestRay or StartExpensiveSynchronousShapeTestLosProbe 4 | local function RayCast(origin, destination, flags) 5 | local ray = ShapeTestRay(origin.x, origin.y, origin.z, destination.x, destination.y, destination.z, flags, nil, 0) 6 | return GetShapeTestResult(ray) 7 | end 8 | 9 | local function randomFloat(lower, greater) 10 | return lower + math.random() * (greater - lower); 11 | end 12 | 13 | local function drawLaser(origin, destination, r, g, b, a) 14 | DrawLine(origin, destination, r, g, b, a) 15 | end 16 | 17 | -- Calculates the linearly interpreted point along the line from "fromPoint" to "toPoint" 18 | -- as a percentage between deltaTime and travelTimeBetweenTargets 19 | local function calculateCurrentPoint(fromPoint, toPoint, deltaTime, travelTimeBetweenTargets) 20 | local desiredDirection = toPoint - fromPoint 21 | local desiredDirectionDist = #desiredDirection 22 | local percentOfTravelTime = deltaTime / (travelTimeBetweenTargets * 1000) 23 | local distance = math.min(desiredDirectionDist * percentOfTravelTime, desiredDirectionDist) 24 | return fromPoint + (norm(desiredDirection) * distance) 25 | end 26 | 27 | local function getNextToIndex(fromIndex, targetPointCount, randomTargetSelection) 28 | local toIndex = fromIndex 29 | if randomTargetSelection then 30 | while toIndex == fromIndex do 31 | toIndex = math.random(1, targetPointCount) 32 | end 33 | else 34 | toIndex = (fromIndex % targetPointCount) + 1 35 | end 36 | return toIndex 37 | end 38 | 39 | function Laser.new(originPoint, targetPoints, options) 40 | local self = {} 41 | options = options or {} 42 | assert(options.color == nil or #options.color == 4, "Laser color must have four values {r, g, b, a}") 43 | 44 | self.name = options.name 45 | 46 | local visible = true 47 | local moving = true 48 | local active = false 49 | local r, g, b, a = 255, 0, 0, 255 50 | if options.color then r, g, b, a = table.unpack(options.color) end 51 | local extensionEnabled = true 52 | if options.extensionEnabled ~= nil then extensionEnabled = options.extensionEnabled end 53 | local randomTargetSelection = true 54 | if options.randomTargetSelection ~= nil then randomTargetSelection = options.randomTargetSelection end 55 | local maxDistance = options.maxDistance or 20.0 56 | local travelTimeBetweenTargets = options.travelTimeBetweenTargets or {} 57 | local minTravelTimeBetweenTargets = travelTimeBetweenTargets[1] or 1.0 58 | local maxTravelTimeBetweenTargets = travelTimeBetweenTargets[2] or 1.0 59 | local waitTimeAtTargets = options.waitTimeAtTargets or {} 60 | local minWaitTimeAtTargets = waitTimeAtTargets ~= nil and waitTimeAtTargets[1] or 0.0 61 | local maxWaitTimeAtTargets = waitTimeAtTargets ~= nil and waitTimeAtTargets[2] or 0.0 62 | local onPlayerHitCb, playerBeingHit = nil, false 63 | 64 | function self.getActive() return active end 65 | function self.setActive(toggle) 66 | if active == toggle then return end 67 | active = toggle 68 | if active then 69 | if type(originPoint) == "vector3" then self._startLaser() 70 | elseif type(originPoint) == "table" then self._startMultiOriginLaser() end 71 | end 72 | end 73 | 74 | function self.getVisible() return visible end 75 | function self.setVisible(toggle) 76 | if visible == toggle then return end 77 | visible = toggle 78 | end 79 | 80 | function self.getMoving() return moving end 81 | function self.setMoving(toggle) 82 | if moving == toggle then return end 83 | moving = toggle 84 | end 85 | 86 | function self.getColor() return r, g, b, a end 87 | function self.setColor(_r, _g, _b, _a) 88 | if type(_r) ~= "number" or type(_g) ~= "number" or type(_b) ~= "number" or type(_a) ~= "number" then 89 | error("(r, g, b, a) must all be integers " .. string.format("{r = %s, g = %s, b = %s, a = %s}", _r, _g, _b, _a)) 90 | end 91 | r, g, b, a = _r, _g, _b, _a 92 | end 93 | 94 | function self.onPlayerHit(cb) 95 | onPlayerHitCb = cb 96 | playerBeingHit = false 97 | end 98 | 99 | function self.clearOnPlayerHit() 100 | onPlayerHitCb = nil 101 | playerBeingHit = false 102 | end 103 | 104 | function self._onPlayerHitTest(origin, destination) 105 | local _, hit, hitPos, _, hitEntity = RayCast(origin, destination, 12) 106 | local newPlayerBeingHit = hit and hitEntity == PlayerPedId() 107 | if newPlayerBeingHit ~= playerBeingHit then 108 | playerBeingHit = newPlayerBeingHit 109 | onPlayerHitCb(playerBeingHit, hitPos) 110 | end 111 | end 112 | 113 | function self._startLaser() 114 | if #targetPoints == 1 then 115 | Citizen.CreateThread(function () 116 | local direction = norm(targetPoints[1] - originPoint) 117 | local destination = originPoint + direction * maxDistance 118 | while active do 119 | if visible then 120 | drawLaser(originPoint, destination, r, g, b, a) 121 | if onPlayerHitCb then 122 | self._onPlayerHitTest(originPoint, destination) 123 | end 124 | end 125 | Wait(0) 126 | end 127 | end) 128 | else 129 | Citizen.CreateThread(function () 130 | local deltaTime = 0 131 | local fromIndex = 1 132 | local toIndex = 2 133 | if randomTargetSelection then 134 | fromIndex = math.random(1, #targetPoints) 135 | toIndex = getNextToIndex(fromIndex, #targetPoints, randomTargetSelection) 136 | end 137 | local waiting = false 138 | local waitTime = 0 139 | local currentTravelTime = randomFloat(minTravelTimeBetweenTargets, maxTravelTimeBetweenTargets) 140 | while active do 141 | local fromPoint = targetPoints[fromIndex] 142 | local toPoint = targetPoints[toIndex] 143 | local currentPoint = calculateCurrentPoint(fromPoint, toPoint, deltaTime, currentTravelTime) 144 | local currentDirection = norm(currentPoint - originPoint) 145 | if visible then 146 | local destination = currentPoint 147 | if extensionEnabled then 148 | destination = originPoint + currentDirection * maxDistance 149 | end 150 | drawLaser(originPoint, destination, r, g, b, a) 151 | if onPlayerHitCb then 152 | self._onPlayerHitTest(originPoint, destination) 153 | end 154 | end 155 | if moving and not waiting then 156 | if #(toPoint - currentPoint) < 0.001 then 157 | deltaTime = 0 158 | fromIndex = toIndex 159 | toIndex = getNextToIndex(fromIndex, #targetPoints, randomTargetSelection) 160 | currentTravelTime = randomFloat(minTravelTimeBetweenTargets, maxTravelTimeBetweenTargets) 161 | if minWaitTimeAtTargets > 0.0 or maxWaitTimeAtTargets > 0.0 then 162 | waiting = true 163 | waitTime = randomFloat(minWaitTimeAtTargets, maxWaitTimeAtTargets) * 1000 164 | end 165 | end 166 | deltaTime = deltaTime + (GetFrameTime() * 1000) 167 | elseif waiting then 168 | waitTime = waitTime - (GetFrameTime() * 1000) 169 | if waitTime <= 0.0 then waiting = false end 170 | end 171 | Wait(0) 172 | end 173 | end) 174 | end 175 | end 176 | 177 | function self._startMultiOriginLaser() 178 | assert(#originPoint == #targetPoints, "Multi-origin laser must have same number of origin and target points") 179 | assert(#originPoint > 1 and #targetPoints > 1, "Multi-origin laser must have more than one origin and target points") 180 | 181 | Citizen.CreateThread(function () 182 | local deltaTime = 0 183 | local fromIndex = 1 184 | local toIndex = 2 185 | local step = 1 186 | local waiting = false 187 | local waitTime = 0 188 | local currentTravelTime = randomFloat(minTravelTimeBetweenTargets, maxTravelTimeBetweenTargets) 189 | while active do 190 | local fromTargetPoint = targetPoints[fromIndex] 191 | local toTargetPoint = targetPoints[toIndex] 192 | local currentTargetPoint = calculateCurrentPoint(fromTargetPoint, toTargetPoint, deltaTime, currentTravelTime) 193 | local fromOriginPoint = originPoint[fromIndex] 194 | local toOriginPoint = originPoint[toIndex] 195 | local currentOriginPoint = calculateCurrentPoint(fromOriginPoint, toOriginPoint, deltaTime, currentTravelTime) 196 | 197 | if visible then 198 | drawLaser(currentOriginPoint, currentTargetPoint, r, g, b, a) 199 | if onPlayerHitCb then 200 | self._onPlayerHitTest(currentOriginPoint, currentTargetPoint) 201 | end 202 | end 203 | if moving and not waiting then 204 | if #(currentTargetPoint - toTargetPoint) < 0.001 then 205 | deltaTime = 0 206 | if toIndex == 1 or toIndex == #originPoint then 207 | step = step * -1 208 | fromIndex = toIndex 209 | toIndex = fromIndex + step 210 | else 211 | fromIndex = fromIndex + step 212 | toIndex = toIndex + step 213 | end 214 | currentTravelTime = randomFloat(minTravelTimeBetweenTargets, maxTravelTimeBetweenTargets) 215 | if minWaitTimeAtTargets > 0.0 or maxWaitTimeAtTargets > 0.0 then 216 | waiting = true 217 | waitTime = randomFloat(minWaitTimeAtTargets, maxWaitTimeAtTargets) * 1000 218 | end 219 | end 220 | deltaTime = deltaTime + (GetFrameTime() * 1000) 221 | elseif waiting then 222 | waitTime = waitTime - (GetFrameTime() * 1000) 223 | if waitTime <= 0.0 then waiting = false end 224 | end 225 | Wait(0) 226 | end 227 | end) 228 | end 229 | 230 | return self 231 | end 232 | --------------------------------------------------------------------------------