mirror of
https://github.com/jimathy/jim_bridge.git
synced 2026-08-17 05:56:02 +01:00
Move GTA notify, target, skillcheck to /ui_modules
I've separated the built-in custom native GTA Notifications, skillcheck and draw text targets to separate files to be loaded along side `jim_bridge`
This now makes it so they only load once instead of with every single script, and each one can call on it
This change also makes these systems able to be called outside of `jim_bridge`
For example in my test server I'm currently using:
```lua
function QBCore.Functions.Notify(text, texttype, length, icon)
CreateThread(function()
exports.jim_bridge:Notify(nil, text, icon)
end)
end
```
This commit is contained in:
120
ui_modules/notifications.lua
Normal file
120
ui_modules/notifications.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
local notifications = {}
|
||||
local spacing = 60 -- vertical spacing between notifications
|
||||
local activeDrawing = false -- flag for drawing loop state
|
||||
|
||||
if not HasStreamedTextureDictLoaded("timerbars") then
|
||||
while not HasStreamedTextureDictLoaded("timerbars") do
|
||||
RequestStreamedTextureDict("timerbars")
|
||||
Wait(5)
|
||||
end
|
||||
end
|
||||
|
||||
local notifTypes = {
|
||||
success = "✔️",
|
||||
error = "❌",
|
||||
warning = "⚠️",
|
||||
police = "🚓",
|
||||
ambulance = "🚑",
|
||||
}
|
||||
|
||||
function gtaNotify(title, message, emoji, src)
|
||||
local notif = {
|
||||
title = title,
|
||||
message = message,
|
||||
emoji = notifTypes[emoji] or "❔",
|
||||
state = "enter",
|
||||
startTime = GetGameTimer(),
|
||||
progress = 0,
|
||||
holdTime = 8000,
|
||||
slideDuration = 100,
|
||||
currentOffset = 0,
|
||||
}
|
||||
table.insert(notifications, notif)
|
||||
|
||||
-- Activate drawing loop if not already running
|
||||
-- This allows it to be silent until the first notifcation is called, then the loop starts
|
||||
if not activeDrawing then
|
||||
activeDrawing = true
|
||||
StartDrawingLoop()
|
||||
end
|
||||
end
|
||||
|
||||
-- Drawing loop as a separate controlled thread
|
||||
function StartDrawingLoop()
|
||||
CreateThread(function()
|
||||
while #notifications > 0 do
|
||||
local currentTime = GetGameTimer()
|
||||
|
||||
-- Update notifications vertical offset
|
||||
for i, notif in ipairs(notifications) do
|
||||
local target = (#notifications - i) * spacing
|
||||
notif.currentOffset += (target - notif.currentOffset) * 0.1
|
||||
end
|
||||
|
||||
for i = #notifications, 1, -1 do
|
||||
local notif = notifications[i]
|
||||
|
||||
if notif.state == "enter" then
|
||||
local elapsed = currentTime - notif.startTime
|
||||
notif.progress = math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress >= 1.0 then
|
||||
notif.state = "hold"
|
||||
notif.holdStart = currentTime
|
||||
end
|
||||
elseif notif.state == "hold" then
|
||||
if currentTime - notif.holdStart >= notif.holdTime then
|
||||
notif.state = "exit"
|
||||
notif.exitStart = currentTime
|
||||
end
|
||||
elseif notif.state == "exit" then
|
||||
local elapsed = currentTime - notif.exitStart
|
||||
notif.progress = 1.0 - math.min(elapsed / notif.slideDuration, 1.0)
|
||||
if notif.progress <= 0 then
|
||||
table.remove(notifications, i)
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
local startX, targetX = 1.0, 0.8
|
||||
local posX = startX - (startX - targetX) * notif.progress
|
||||
local posY = 0.05 + (notif.currentOffset / 1080)
|
||||
|
||||
-- Background sprite
|
||||
DrawSprite("timerbars", "all_black_bg", posX + 0.11, posY + 0.025, 0.2, 0.053, 0.0, 255, 255, 255, 255)
|
||||
|
||||
-- Title text
|
||||
local moveMessage = false
|
||||
if not notif.title or notif.title == "" then
|
||||
moveMessage = true
|
||||
else
|
||||
drawNotiText(8, 0.4, vec2(0.75, 0.975), notif.title, vec2(posX, posY))
|
||||
end
|
||||
|
||||
-- Message text
|
||||
drawNotiText(4, 0.3, vec2(0.75, 0.975), notif.message, vec2(posX, posY + (moveMessage and 0.015 or 0.03)))
|
||||
|
||||
-- Emoji
|
||||
drawNotiText(0, 0.3, vec2(0.75, 0.995), notif.emoji, vec2(posX, posY + 0.015))
|
||||
|
||||
::continue::
|
||||
end
|
||||
Wait(0)
|
||||
end
|
||||
activeDrawing = false -- No notifications left, pause drawing
|
||||
end)
|
||||
end
|
||||
|
||||
function drawNotiText(font, scale, wrap, string, pos)
|
||||
SetTextFont(font)
|
||||
SetTextScale(scale, scale)
|
||||
SetTextWrap(wrap.x, wrap.y)
|
||||
SetTextJustification(2)
|
||||
SetTextColour(255, 255, 255, 255)
|
||||
SetTextOutline()
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(string)
|
||||
DrawText(pos.x, pos.y)
|
||||
end
|
||||
|
||||
RegisterNetEvent("jim-bridge:Notify", gtaNotify)
|
||||
exports("Notify", gtaNotify)
|
||||
121
ui_modules/skillcheck.lua
Normal file
121
ui_modules/skillcheck.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
local activeSkillCheck = false
|
||||
|
||||
function gtaSkillCheck()
|
||||
if activeSkillCheck then return end
|
||||
local result = false
|
||||
local successes = 0
|
||||
local barsRequired = 3
|
||||
|
||||
for bar = 1, barsRequired do
|
||||
activeSkillCheck = true
|
||||
local width, height = 0.2, 0.01
|
||||
local x, y = 0.5, 0.8
|
||||
|
||||
-- Random highlighted zone
|
||||
local highlightSize = math.random(10, 20) / 100
|
||||
local highlightStart = math.random(10, 50) / 100
|
||||
local highlightEnd = highlightStart + highlightSize
|
||||
local highlightAlpha = 0
|
||||
local cursorPos = 0.0
|
||||
local cursorSpeed = 0.025
|
||||
local movingRight = true
|
||||
|
||||
while activeSkillCheck do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect
|
||||
highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha
|
||||
|
||||
-- Draw highlighted zone (success area) with pulsing effect
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha )
|
||||
-- Draw moving cursor
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
|
||||
-- Move cursor
|
||||
if movingRight then
|
||||
cursorPos += cursorSpeed
|
||||
if cursorPos >= 1.0 then movingRight = false end
|
||||
else
|
||||
cursorPos -= cursorSpeed
|
||||
if cursorPos <= 0.0 then movingRight = true end
|
||||
end
|
||||
|
||||
if IsControlJustPressed(0, 177) then -- Backspace to cancel
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255)
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
|
||||
drawSuccessText(x, y, "Failed", 228, 52, 52)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check for keypress (E)
|
||||
if IsControlJustPressed(0, 38) then
|
||||
activeSkillCheck = false
|
||||
result = cursorPos >= highlightStart and cursorPos <= highlightEnd
|
||||
if result then
|
||||
PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true)
|
||||
else
|
||||
PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1)
|
||||
end
|
||||
local displayTime = GetGameTimer() + 2000
|
||||
while GetGameTimer() < displayTime do
|
||||
Wait(0)
|
||||
|
||||
createScaleBars(x, y, width, height)
|
||||
|
||||
-- Draw highlighted zone
|
||||
DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180)
|
||||
|
||||
-- Draw stationary cursor at result position
|
||||
DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255)
|
||||
-- Display result text
|
||||
drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52)
|
||||
end
|
||||
if result then
|
||||
successes += 1
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
activeSkillCheck = false
|
||||
print("^5GTAUI^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7")
|
||||
return successes == barsRequired
|
||||
end
|
||||
|
||||
function drawSuccessText(x, y, text, r, g, b)
|
||||
SetTextFont(8)
|
||||
SetTextScale(0.45, 0.45)
|
||||
SetTextColour(r, g, b, 255)
|
||||
SetTextDropshadow(0, 0, 0, 0, 255)
|
||||
SetTextEdge(2, 0, 0, 0, 150)
|
||||
SetTextDropShadow()
|
||||
SetTextOutline()
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(true)
|
||||
SetTextEntry("STRING")
|
||||
AddTextComponentString(text)
|
||||
DrawText(x, y + 0.03)
|
||||
end
|
||||
|
||||
function createScaleBars(x, y, width, height)
|
||||
-- Draw background box
|
||||
DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255)
|
||||
DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255)
|
||||
-- Draw full bar (dark background)
|
||||
DrawRect(x, y, width, height, 100, 100, 100, 255)
|
||||
end
|
||||
|
||||
exports("skillCheck", gtaSkillCheck)
|
||||
318
ui_modules/target.lua
Normal file
318
ui_modules/target.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
-- Global Key Table, defined once.
|
||||
---
|
||||
local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 }
|
||||
|
||||
-- Mapping of key codes to human-readable key names.
|
||||
local Keys = {
|
||||
[322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5",
|
||||
[167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10",
|
||||
[243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5",
|
||||
[159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=",
|
||||
[177] = "BACKSPACE", [37] = "TAB",
|
||||
[44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y",
|
||||
[303] = "U", [199] = "P",
|
||||
[39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS",
|
||||
[34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G",
|
||||
[74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT",
|
||||
[20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N",
|
||||
[244] = "M", [82] = ",", [81] = "."
|
||||
}
|
||||
-- Tables for storing created targets.
|
||||
local TextTargets = {} -- For fallback DrawText3D targets.
|
||||
local targetEntities = {} -- For entity targets.
|
||||
|
||||
function createEntityTarget(entity, opts, dist)
|
||||
startTargetLoop()
|
||||
targetEntities[#targetEntities + 1] = entity
|
||||
local entityCoords = GetEntityCoords(entity)
|
||||
|
||||
local existingTarget = nil
|
||||
for _, target in pairs(TextTargets) do
|
||||
if #(target.coords - entityCoords) < 0.01 then
|
||||
existingTarget = target
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if existingTarget then
|
||||
for i = 1, #opts do
|
||||
local key = KEY_TABLE[#existingTarget.options + i]
|
||||
opts[i].key = key
|
||||
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
|
||||
existingTarget.options[#existingTarget.options + 1] = opts[i]
|
||||
end
|
||||
updateCachedText(existingTarget)
|
||||
else
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
TextTargets[entity] = {
|
||||
coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z),
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function createZoneTarget(data, opts, dist)
|
||||
startTargetLoop()
|
||||
local existingTarget = nil
|
||||
for _, target in pairs(TextTargets) do
|
||||
if #(target.coords - data[2]) < 0.01 then
|
||||
existingTarget = target
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if existingTarget then
|
||||
for i = 1, #opts do
|
||||
local key = KEY_TABLE[#existingTarget.options + i]
|
||||
opts[i].key = key
|
||||
existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label
|
||||
existingTarget.options[#existingTarget.options + 1] = opts[i]
|
||||
end
|
||||
updateCachedText(existingTarget)
|
||||
else
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
TextTargets[data[1]] = {
|
||||
coords = data[2],
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
end
|
||||
return data[1]
|
||||
end
|
||||
|
||||
function createModelTarget(models, opts, dist)
|
||||
startTargetLoop()
|
||||
if type(models) ~= "table" then
|
||||
models = { models }
|
||||
end
|
||||
|
||||
local tempText = {}
|
||||
for i = 1, #opts do
|
||||
opts[i].key = KEY_TABLE[i]
|
||||
tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label
|
||||
end
|
||||
|
||||
local keyStr = ""
|
||||
for i, m in ipairs(models) do
|
||||
keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "")
|
||||
end
|
||||
local targetKey = "model_" .. keyStr
|
||||
|
||||
TextTargets[targetKey] = {
|
||||
models = models,
|
||||
buttontext = tempText,
|
||||
options = opts,
|
||||
dist = dist,
|
||||
coords = vec3(0, 0, 0),
|
||||
text = table.concat(tempText, "\n")
|
||||
}
|
||||
|
||||
return targetKey
|
||||
end
|
||||
|
||||
function removeEntityTarget(entity)
|
||||
TextTargets[entity] = nil
|
||||
end
|
||||
|
||||
function removeZoneTarget(target)
|
||||
TextTargets[target] = nil
|
||||
end
|
||||
|
||||
function removeModelTarget(model)
|
||||
TextTargets[model] = nil
|
||||
end
|
||||
|
||||
exports("createEntityTarget", createEntityTarget)
|
||||
exports("createZoneTarget", createZoneTarget)
|
||||
exports("createModelTarget", createModelTarget)
|
||||
|
||||
exports("removeEntityTarget", removeEntityTarget)
|
||||
exports("removeZoneTarget", removeZoneTarget)
|
||||
exports("removeModelTarget", removeModelTarget)
|
||||
|
||||
|
||||
-------------------------------------------------------------
|
||||
-- Fallback: DrawText3D Targets (Experimental)
|
||||
-------------------------------------------------------------
|
||||
local started = false
|
||||
function startTargetLoop()
|
||||
if started then return end
|
||||
Config = {
|
||||
System = {
|
||||
|
||||
}
|
||||
}
|
||||
started = true
|
||||
local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua')))
|
||||
fileLoader()
|
||||
-- Model Entity Refresher
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local pedCoords = GetEntityCoords(PlayerPedId())
|
||||
for _, target in pairs(TextTargets) do
|
||||
if target.models then
|
||||
for _, model in ipairs(target.models) do
|
||||
local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false)
|
||||
if entity and entity ~= 0 then
|
||||
target.entity = entity
|
||||
target.coords = GetEntityCoords(entity)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Wait(3000) -- Refresh every 3s
|
||||
end
|
||||
end)
|
||||
|
||||
-- Main Target Loop
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local ped = PlayerPedId()
|
||||
local pedCoords = GetEntityCoords(ped)
|
||||
local camCoords = GetGameplayCamCoord()
|
||||
local camRot = GetGameplayCamRot(2)
|
||||
local camForward = RotationToDirection(camRot)
|
||||
|
||||
local closestTarget, closestDist, targetEntity = nil, math.huge, nil
|
||||
|
||||
for _, target in pairs(TextTargets) do
|
||||
local coords = target.coords
|
||||
if coords then
|
||||
local dist = #(pedCoords - coords)
|
||||
if dist <= target.dist then
|
||||
local normVec = normalizeVector(coords - camCoords)
|
||||
local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z
|
||||
|
||||
if dot > 0.5 and dist < closestDist then
|
||||
closestTarget = target
|
||||
closestDist = dist
|
||||
targetEntity = target.entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, target in pairs(TextTargets) do
|
||||
if not target.coords then goto continue end
|
||||
local dist = #(pedCoords - target.coords)
|
||||
if dist > target.dist then goto continue end
|
||||
local isClosest = (target == closestTarget)
|
||||
|
||||
for i, opt in ipairs(target.options) do
|
||||
if IsControlJustPressed(0, opt.key) and isClosest then
|
||||
if (not target.canInteract or target.canInteract()) and
|
||||
(not opt.item or hasItem(opt.item)) and
|
||||
(not opt.job or hasJob(opt.job, nil)) then
|
||||
if opt.onSelect then opt.onSelect(targetEntity) end
|
||||
if opt.action then opt.action(targetEntity) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local baseZ, lineHeight = target.coords.z + 1.0, -0.16
|
||||
local lineOffset = 0
|
||||
|
||||
for i, opt in ipairs(target.options) do
|
||||
if (not target.canInteract or target.canInteract()) and
|
||||
(not opt.item or hasItem(opt.item)) and
|
||||
(not opt.job or hasJob(opt.job, nil)) then
|
||||
DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest)
|
||||
lineOffset = lineOffset + 1
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
Wait(1) -- Throttled
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
function DrawText3D(coord, text, highlight)
|
||||
SetTextScale(0.30, 0.30)
|
||||
SetTextFont(0)
|
||||
SetTextProportional(1)
|
||||
SetTextColour(255, 255, 255, 215)
|
||||
SetTextEntry("STRING")
|
||||
SetTextCentre(true)
|
||||
|
||||
local totalLength = string.len(text)
|
||||
local textMaxLength = 99 -- max 99
|
||||
local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text
|
||||
AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text)
|
||||
SetDrawOrigin(coord.x, coord.y, coord.z, 0)
|
||||
DrawText(0.0, 0.0)
|
||||
local count, length = GetLineCountAndMaxLength(text)
|
||||
|
||||
local padding = 0.005
|
||||
local heightFactor = (count / 43) + padding
|
||||
local weightFactor = (length / 150) + padding
|
||||
|
||||
local height = (heightFactor / 2) - padding / 1
|
||||
local width = (weightFactor / 2) - padding / 1
|
||||
|
||||
DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150)
|
||||
ClearDrawOrigin()
|
||||
end
|
||||
|
||||
--- Calculates the number of lines and the maximum line length from the given text.
|
||||
---
|
||||
--- @param text string The text to analyze.
|
||||
--- @return number, number The line count and maximum line length.
|
||||
---
|
||||
--- @usage
|
||||
--- ```lua
|
||||
--- local count, maxLen = GetLineCountAndMaxLength("Hello World")
|
||||
--- ```
|
||||
function GetLineCountAndMaxLength(text)
|
||||
local lineCount, maxLength = 0, 0
|
||||
for line in text:gmatch("[^\n]+") do
|
||||
lineCount += 1
|
||||
local lineLength = string.len(line)
|
||||
if lineLength > maxLength then
|
||||
maxLength = lineLength
|
||||
end
|
||||
end
|
||||
if lineCount == 0 then lineCount = 1 end
|
||||
return lineCount, maxLength
|
||||
end
|
||||
|
||||
|
||||
function RotationToDirection(rot)
|
||||
local adjust = math.pi / 180
|
||||
return vec3(
|
||||
-math.sin(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
|
||||
math.cos(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)),
|
||||
math.sin(adjust * rot.x)
|
||||
)
|
||||
end
|
||||
|
||||
function normalizeVector(vec)
|
||||
local len = math.sqrt(vec.x^2 + vec.y^2 + vec.z^2)
|
||||
if len ~= 0 then
|
||||
return vec3(vec.x / len, vec.y / len, vec.z / len)
|
||||
else
|
||||
return vec3(0, 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper to update cached text.
|
||||
function updateCachedText(target)
|
||||
target.text = table.concat(target.buttontext, "\n")
|
||||
end
|
||||
Reference in New Issue
Block a user