Compare commits

...

3 Commits

Author SHA1 Message Date
Jim Shield
145e3aa156 add new module ropeControl.lua
**Needs work** but adds the ability to easily create ropes/wires from one coord to another
2025-09-27 13:01:09 +01:00
Jim Shield
bb176addbf Create setStress function
This allows per framework stress functions to be set, including ESX

For ESX the math is done in this function as their values are insanely high instead of 0 - 100
2025-09-27 12:35:56 +01:00
Jim Shield
178040348e Rework SingleProgress slightly
Realised it wasn't doing the math to take the correct amount of required items, so this fixes it

also if `SingleProgress` is enabled, it won't show the "Ingredient Progress bars" any more
2025-09-27 12:34:46 +01:00
5 changed files with 355 additions and 44 deletions

View File

@@ -323,6 +323,7 @@ end
--- ```
function makeItem(data)
local Ped = PlayerPedId()
if CraftLock then return end
CraftLock = true
data.stashName = data.stashTable or data.stashName
@@ -339,40 +340,42 @@ function makeItem(data)
local canReturn = true
local crafted, crafting = true, true
local cam = createTempCam(PlayerPedId(), data.coords)
local cam = createTempCam(Ped, data.coords)
startTempCam(cam)
-- Calculate total bartime if SingleProgress is enabled
local totalBartime = bartime * craftAmount
local totalBartime = (bartime * craftAmount)
-- Run ingredient check and usage separately first
for i = 1, craftAmount do
for k, v in pairs(data.craft) do
if not excludeKeys[k] and type(v) == "table" then
for l, b in pairs(v) do
if isInventoryOpen() then
print("^1Error^7: ^2Inventory is open, you tried to break things")
stopTempCam()
ClearPedTasks(PlayerPedId())
if canReturn then craftingMenu(data) end
CraftLock = false
return
if not Config.Crafting.SingleProgress then -- if SingleProgress is disabled, dont do ingredient progressbars
-- Run ingredient check and usage separately first
for i = 1, craftAmount do
for k, v in pairs(data.craft) do
if not excludeKeys[k] and type(v) == "table" then
for l, b in pairs(v) do
if isInventoryOpen() then
print("^1Error^7: ^2Inventory is open, you tried to break things")
stopTempCam()
ClearPedTasks(Ped)
if canReturn then craftingMenu(data) end
CraftLock = false
return
end
if crafting and progressBar({
label = "Using "..b.." "..getItemLabel(l),
time = 800,
cancel = true,
dict = 'pickup_object',
anim = "putdown_low",
flag = 49,
icon = l,
}) then
TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[l], "use", b)
else
crafted, crafting = false, false
break
end
Wait(200)
end
if crafting and progressBar({
label = "Using "..b.." "..getItemLabel(l),
time = 1000,
cancel = true,
dict = 'pickup_object',
anim = "putdown_low",
flag = 49,
icon = l,
}) then
TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[l], "use", b)
else
crafted, crafting = false, false
break
end
Wait(200)
end
end
end
@@ -380,7 +383,7 @@ function makeItem(data)
if not crafted then
stopTempCam()
ClearPedTasks(PlayerPedId())
ClearPedTasks(Ped)
if canReturn then craftingMenu(data) end
CraftLock = false
return
@@ -391,11 +394,11 @@ function makeItem(data)
local craftProp = nil
if prop then
craftProp = makeProp({ prop = prop.model, coords = vec4(0, 0, 0, 0), true, true })
AttachEntityToEntity(craftProp, PlayerPedId(), GetPedBoneIndex(PlayerPedId(), prop.bone), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 1, true)
AttachEntityToEntity(craftProp, Ped, GetPedBoneIndex(Ped, prop.bone), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 1, true)
end
if data.sound then
local s = data.sound
PlaySoundFromEntity(s.soundId, s.audioName, PlayerPedId(), s.audioRef, true, 0)
PlaySoundFromEntity(s.soundId, s.audioName, Ped, s.audioRef, true, 0)
end
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount,
@@ -408,6 +411,10 @@ function makeItem(data)
request = true,
}) then
data.craft.amount = craftAmount
for k, v in pairs(data.craft[data.item]) do
-- multiply igredient requirement in sent crafting table for removal
data.craft[data.item][k] = (v * craftAmount)
end
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken)
currentToken = nil -- clear client cached token
-- handle metadata and experience in a single go
@@ -422,7 +429,7 @@ function makeItem(data)
if data.craftable.Recipes[1].oneUse == true then
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
local breakId = GetSoundId()
PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
canReturn = false
end
if data.sound then
@@ -459,7 +466,7 @@ function makeItem(data)
if data.craftable.Recipes[1].oneUse == true then
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
local breakId = GetSoundId()
PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
canReturn = false
end
if data.requiredItemfunc then
@@ -475,7 +482,7 @@ function makeItem(data)
stopTempCam()
CraftLock = false
if canReturn then craftingMenu(data) end
ClearPedTasks(PlayerPedId())
ClearPedTasks(Ped)
end

View File

@@ -2390,17 +2390,17 @@ if isServer() then
end
RegisterNetEvent(getScript()..":openGrabBox", function(data)
if isStarted(OXInv) then
return
end
local id = ""
if data.metadata then
id = data.metadata.id
elseif data.info then
id = data.info.id
end
local Ped = PlayerPedId()
local id = data.metadata and data.metadata.id or data.info and data.info.id or ""
if isStarted(OXInv) then
return
end
openStash({
stash = id,
coords = GetEntityCoords(Ped)
})
end)

255
shared/make/ropeControl.lua Normal file
View File

@@ -0,0 +1,255 @@
if not isServer() then
local RopeCache = {
nextId = 1,
byId = {},
byHandle = {},
}
function loadRopeTextures()
RopeLoadTextures()
local tries = 0
while not RopeAreTexturesLoaded() and tries < 200 do
Wait(0)
tries += 1
end
return RopeAreTexturesLoaded()
end
function maybeUnloadTextures()
local anyLeft = false
local all = GetAllRopes()
if all and type(all) == "table" then
for _, _ in ipairs(all) do anyLeft = true break end
end
if not anyLeft then
RopeUnloadTextures()
end
end
function registerRope(ropeHandle, startCoords, endCoords, opts)
if not ropeHandle or ropeHandle == 0 then return nil end
local id = RopeCache.nextId
RopeCache.nextId = id + 1
local rec = {
handle = ropeHandle,
startCoords = startCoords,
endCoords = endCoords,
type = opts.ropeType,
rate = opts.lengthChangeRate,
breakable = opts.breakable,
createdAt = GetGameTimer()
}
RopeCache.byId[id] = rec
RopeCache.byHandle[ropeHandle] = id
debugPrint("^5Debug^7: 2Registered Rope^7'^3"..id.."^7' - '^3"..ropeHandle.."^7'")
return id
end
function resolve(id)
if type(id) ~= "number" then return nil, "rope id must be a number" end
local rec = RopeCache.byId[id]
if not rec then return nil, "rope id not found" end
if not DoesRopeExist(rec.handle) then
-- stale; clean mapping
RopeCache.byHandle[rec.handle] = nil
RopeCache.byId[id] = nil
return nil, "rope no longer exists"
end
return rec
end
--- Create a rope pinned between two coordinates.
--- @param startCoords vector3|table start coord
--- @param endCoords vector3|table end coord
--- @param opts table|nil { ropeType=0..7, breakable=false, collision=true, lockFromFront=false, lengthChangeRate=1.0, timeMultiplier=1.0, slack=0.0, preset=nil }
--- @return integer ropeId
function ropeCreateLine(startCoords, endCoords, opts)
startCoords = type(startCoords) == "number" and GetEntityCoords(startCoords) or startCoords
endCoords = type(endCoords) == "number" and GetEntityCoords(endCoords) or endCoords
opts = opts or {}
local length = #(startCoords - endCoords)
local mid = (startCoords + endCoords) * 0.5
local ropeType = opts.ropeType or 1 -- 0..7 (see ropedata.xml types)
local initLength = length + (opts.slack or 0.0)
local maxLength = initLength -- allow droop equal to init
local minLength = 0.0
local lengthChangeRate = opts.lengthChangeRate or 1.0
local collisionOn = (opts.collision ~= false)
local lockFromFront = (opts.lockFromFront == true)
local timeMultiplier = opts.timeMultiplier or 1.0
local breakable = (opts.breakable == true)
local rope = AddRope(mid.x, mid.y, mid.z, 0.0, 0.0, 0.0, maxLength, ropeType, initLength, minLength, lengthChangeRate, false, collisionOn, lockFromFront, timeMultiplier, breakable, 0)
if not rope or rope == 0 or not DoesRopeExist(rope) then
debugPrint("^1AddRope failed")
return nil
end
-- Optional preset (e.g., "ropeFamily3") if provided.
if opts.preset then
-- Best-effort; if invalid it just won't change.
pcall(function() LoadRopeData(rope, tostring(opts.preset)) end)
end
ActivatePhysics(rope)
-- Pin both ends to the given coords.
local count = GetRopeVertexCount(rope)
if count and count >= 2 then
PinRopeVertex(rope, 0, startCoords.x, startCoords.y, startCoords.z)
PinRopeVertex(rope, count - 1, endCoords.x, endCoords.y, endCoords.z)
end
local id = registerRope(rope, startCoords, endCoords, {
ropeType = ropeType,
lengthChangeRate = lengthChangeRate,
breakable = breakable
})
return id
end
--- Delete and unregister a rope.
--- @param id integer
function ropeDelete(id)
local rec, err = resolve(id)
if not rec then return false, err end
local rope = rec.handle
if DoesRopeExist(rope) then
DeleteRope(rope)
end
RopeCache.byHandle[rope] = nil
RopeCache.byId[id] = nil
debugPrint(("deleted rope id=%s"):format(id))
maybeUnloadTextures()
return true
end
--- Force rope to a specific length (instant snap).
--- @param id integer
--- @param newLength number
function ropeSetLength(id, newLength)
local rec, err = resolve(id)
if not rec then return false, err end
RopeForceLength(rec.handle, newLength)
return true
end
--- Smoothly wind the rope (shorten over time).
--- @param id integer
function ropeStartWinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StartRopeWinding(rec.handle)
return true
end
--- Stop winding (if winding).
function ropeStopWinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StopRopeWinding(rec.handle)
return true
end
--- Smoothly unwind the rope from the front (lengthen over time).
function ropeStartUnwinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StartRopeUnwindingFront(rec.handle)
return true
end
--- Stop unwinding (if unwinding).
function ropeStopUnwinding(id)
local rec, err = resolve(id)
if not rec then return false, err end
StopRopeUnwindingFront(rec.handle)
return true
end
--- Change the wind speed (length change rate).
--- @param id integer
--- @param rate number (units per sec; try 0.5..5.0)
function ropeSetRate(id, rate)
local rec, err = resolve(id)
if not rec then return false, err end
SetRopeLengthChangeRate(rec.handle, rate)
rec.rate = rate
return true
end
--- Move end endpoint to a new coordinate (re-pins the vertex).
--- @param id integer
--- @param pos vector3|table
function ropeMoveEnd(id, pos)
local rec, err = resolve(id)
if not rec then return false, err end
pos = type(pos) == "number" and GetEntityCoords(pos) or pos
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then return false, "invalid vertex count" end
PinRopeVertex(rope, count - 1, pos.x, pos.y, pos.z)
rec.endCoord = pos
return true
end
--- Move start endpoint to a new coordinate (re-pins the vertex).
--- @param id integer
--- @param pos vector3|table
function ropeMoveStart(id, pos)
local rec, err = resolve(id)
if not rec then return false, err end
pos = type(pos) == "number" and GetEntityCoords(pos) or pos
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then return false, "invalid vertex count" end
PinRopeVertex(rope, 0, pos.x, pos.y, pos.z)
rec.startCoord = pos
return true
end
--- Re-pin both endpoints in one call.
function ropeSetEnds(ropeId, startCoord, endCoord)
startCoord = type(startCoord) == "number" and GetEntityCoords(startCoord) or startCoord
endCoord = type(endCoord) == "number" and GetEntityCoords(endCoord) or endCoord
local rec, err = resolve(ropeId)
if not rec then return false, err end
local rope = rec.handle
local count = GetRopeVertexCount(rope)
if not count or count < 2 then
return false, "invalid vertex count"
end
PinRopeVertex(rope, 0, startCoord.x, startCoord.y, startCoord.z)
PinRopeVertex(rope, count - 1, endCoord.x, endCoord.y, endCoord.z)
rec.startCoord = startCoord
rec.endCoord = endCoord
return true
end
function ropeExists(id)
local rec = RopeCache.byId[id]
return rec and DoesRopeExist(rec.handle) or false
end
onResourceStart(function()
loadRopeTextures()
end)
onResourceStop(function()
for id, rec in pairs(RopeCache.byId) do
if rec.handle and DoesRopeExist(rec.handle) then
DeleteRope(rec.handle)
end
RopeCache.byHandle[rec.handle] = nil
RopeCache.byId[id] = nil
end
maybeUnloadTextures()
end)
end

View File

@@ -29,6 +29,14 @@ local hungerThirstFunc = {
Player.Functions.SetMetaData('hunger', newHunger)
TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst)
end,
setStress =
function(src, stress)
local Player = Core.Functions.GetPlayer(src)
local newStress = Player.PlayerData.metadata.stress + stress
newStress = (newStress >= 100.0 and 100.0)or (newStress <= 0 and 0) or newStress
Player.Functions.SetMetaData('stress', newStress)
TriggerClientEvent('hud:client:UpdateStress', src, newStress)
end,
},
{ framework = QBExport,
@@ -48,6 +56,14 @@ local hungerThirstFunc = {
Player.Functions.SetMetaData('hunger', newHunger)
TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst)
end,
setStress =
function(src, stress)
local Player = Core.Functions.GetPlayer(src)
local newStress = Player.PlayerData.metadata.stress + stress
newStress = (newStress >= 100.0 and 100.0)or (newStress <= 0 and 0) or newStress
Player.Functions.SetMetaData('stress', newStress)
TriggerClientEvent('hud:client:UpdateStress', src, newStress)
end,
},
{ framework = RSGExport,
@@ -67,6 +83,14 @@ local hungerThirstFunc = {
Player.Functions.SetMetaData('hunger', newHunger)
TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst)
end,
setStress =
function(src, stress)
local Player = Core.Functions.GetPlayer(src)
local newStress = Player.PlayerData.metadata.stress + stress
newStress = (newStress >= 100.0 and 100.0) or (newStress <= 0 and 0) or newStress
Player.Functions.SetMetaData('stress', newStress)
TriggerClientEvent('hud:client:UpdateStress', src, newStress)
end,
},
{ framework = ESXExport,
@@ -78,6 +102,10 @@ local hungerThirstFunc = {
function(src, hunger)
TriggerClientEvent('esx_status:add', src, 'hunger', tonumber(hunger) * 10000)
end,
setStress =
function(src, stress)
TriggerClientEvent("esx_status:add", src, "stress", tonumber(stress) * 10000)
end,
},
}
@@ -126,6 +154,26 @@ function setHunger(src, hunger)
end
end
--- Sets the player's stress level.
---
--- @param src number The player's server ID.
--- @param stress number The new stress level.
---
--- @usage
--- ```lua
--- setStress(playerId, 60)
--- ```
function setStress(src, stress)
for i = 1, #hungerThirstFunc do
local framework = hungerThirstFunc[i]
if isStarted(framework.framework) then
debugPrint("^4Debug^7: ^2Adding ^3"..framework.framework.." ^2stress^7: ^3"..stress.." ^2to player^7: ^3"..src.."^7")
framework.setStress(src, stress)
return
end
end
end
--- Server event handler for urinal usage.
--- Decreases player's thirst by a random amount.
RegisterNetEvent(getScript()..":server:Urinal", function()

View File

@@ -176,6 +176,7 @@ for _, v in pairs({ -- This is a specific load order
'scaleEntity.lua',
'vehicles.lua',
'effects.lua',
'make/ropeControl.lua',
--'warmenu.lua',
'_authToken.lua',