Files
jim_bridge/shared/crafting.lua

611 lines
26 KiB
Lua

--[[
Crafting, Selling, and Shop Module
-----------------------------------
This module provides functions for opening crafting menus, handling multi-crafting,
performing the crafting process (with animations and progress bars), selling items,
and opening shop interfaces. It integrates with various inventory and menu systems,
and uses server callbacks to check item carry capacity.
]]
-------------------------------------------------------------
-- Global Variables
-------------------------------------------------------------
CraftLock = false
-------------------------------------------------------------
-- Crafting Menu
-------------------------------------------------------------
--- Opens the crafting menu based on provided data.
--- Checks job restrictions, builds the recipe menu, and opens the menu.
---
--- @param data table Crafting menu configuration containing:
--- - craftable (`table`) Table with Header, Recipes, Anims, and (optionally) craftedItems.
--- - coords (`vector3`) The coordinates where the crafting menu is being opened.
--- - stashTable|stashName (`string\table`) Name(s) of the stash for checking item availability.
--- - job|gang (`string`) Job or gang requirements.
--- - onBack (optional): Function to call when returning.
---
--- @usage
--- ```lua
---craftingMenu({
--- craftable = {
--- Header = "Weapon Crafting",
--- Recipes = {
--- [1] = {
--- ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 },
--- amount = 1,
--- },
--- -- More recipes...
--- },
--- Anims = {
--- animDict = "amb@prop_human_parking_meter@male@idle_a",
--- anim = "idle_a",
--- },
--- },
--- coords = vector3(100.0, 200.0, 300.0),
---stashTable = "crafting_stash",
--- job = "mechanic",
--- onBack = function() print("Returning to previous menu") end,
---})
function craftingMenu(data)
if CraftLock then return end
-- Job or gang check; exit if not authorized.
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
-- Display a temporary "thinking" notification.
if Config.System.Menu == "jim" then
triggerNotify(nil, "Thinking", "info")
else
openMenu({ { header = "Thinking...", icon = "fas fa-hourglass-end", isMenuHeader = true } }, { header = "Crafting Menu" } )
end
-- Normalize stash name.
data.stashName = data.stashTable or data.stashName
local Menu = {}
local Recipes = data.craftable.Recipes
local craftedItems = {}
local tempCarryTable = {}
-- Build a table of all required ingredients (default quantity is 1).
for i = 1, #Recipes do
for k in pairs(Recipes[i]) do
if k == "hasCrafted" and not data.craftable.craftedItems then
craftedItems = GetMetadata(nil, "craftedItems") or {}
data.craftable.craftedItems = craftedItems
end
if k ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" then
tempCarryTable[k] = Recipes[i].amount or 1
end
end
end
-- Check if the player can carry the required items (server callback).
local canCarryTable = triggerCallback(getScript()..':server:canCarry', tempCarryTable)
-- Process each recipe to create menu entries.
for i = 1, #Recipes do
if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
for k, _ in pairs(Recipes[i]) do
local excludeKeys = {
amount = true, metadata = true, description = true, info = true,
job = true, gang = true, oneUse = true, slot = true,
blueprintRef = true, craftingLevel = true, craftedItems = true,
hasCrafted = true, exp = true, anim = true, time = true,
}
if not excludeKeys[k] then
local hasjob = true
if Recipes[i].job then
for l, b in pairs(Recipes[i].job) do
hasjob = hasJob(l, nil, b)
if hasjob then break end
end
end
if hasjob then
local setheader, settext, disable, metadata = "", "", false, (Recipes[i]["metadata"] or Recipes[i]["info"] or nil)
local itemTable = {}
local metaTable = {}
-- Build ingredient details.
for l, b in pairs(Recipes[i][tostring(k)]) do
settext = settext..(settext ~= "" and br or "")..(Items[l] and Items[l].label or "error - "..l)..(b > 1 and " x"..b or "")
metaTable[Items[l] and Items[l].label or "error - "..l] = b
itemTable[l] = b
Wait(0)
end
while not canCarryTable do Wait(0) end
disable = not checkHasItem(data.stashName, itemTable)
setheader = ((metadata and metadata.label) or (Items[tostring(k)] and Items[tostring(k)].label) or "error - "..tostring(k))
..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "")
if not disable then
if not canCarryTable[k] then
setheader = setheader.." 📦"
else
setheader = setheader.." ✔️"
end
elseif not canCarryTable[k] then
setheader = setheader.." 📦"
end
if Recipes[i]["hasCrafted"] ~= nil and craftedItems[k] == nil then
setheader = ""..setheader
end
Menu[#Menu + 1] = {
arrow = not disable and canCarryTable[k],
isMenuHeader = disable or not canCarryTable[k],
icon = invImg((metadata and metadata.image) or tostring(k)),
image = invImg((metadata and metadata.image) or tostring(k)),
header = setheader..((disable or not canCarryTable[k]) and "" or ""),
txt = (isStarted(QBMenuExport) or disable) and settext or nil,
metadata = metaTable,
onSelect = (not disable and canCarryTable[k]) and function()
local transdata = {
item = k,
craft = data.craftable.Recipes[i],
craftable = data.craftable,
coords = data.coords,
stashName = data.stashName,
onBack = data.onBack,
metadata = metadata,
}
if Config.Crafting.MultiCraft then
multiCraft(transdata)
else
makeItem(transdata)
end
end or nil,
}
end
end
Wait(0)
end
end
openMenu(Menu, {
header = data.craftable.Header,
headertxt = data.craftable.Headertxt,
onBack = data.onBack or nil,
canClose = true,
onExit = function() end,
})
lookEnt(data.coords)
end
-------------------------------------------------------------
-- Multi-Craft Menu
-------------------------------------------------------------
--- Opens a menu for selecting the quantity to craft.
---
--- Presents the player with multiple crafting quantities based on Config.Crafting.MultiCraftAmounts.
---
--- @param data table Crafting configuration containing:
--- - item `string`) The item to craft.
--- - craft (`table`) The crafting recipe.
--- - craftable (`table`) Crafting options.
--- - coords (`vector3`) where crafting occurs.
--- - stashName (`string`) The stash name(s) for item availability.
--- - onBack (`function`) Callback when returning.
--- - metadata (`table`) (optional): Metadata for the crafted item.
---
--- @usage
--- ```lua
--- multiCraft({
--- item = "weapon_pistol",
--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
--- craftable = craftingOptions,
--- coords = vector3(100,200,300),
--- stashName = "crafting_stash",
--- onBack = function() craftingMenu(data) end,
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
--- })
--- ```
function multiCraft(data)
local Menu = {}
local amounts = Config.Crafting.MultiCraftAmounts
local metadata = data.metadata or nil
-- Header for the multi-craft menu.
Menu[#Menu + 1] = {
isMenuHeader = true,
icon = invImg(metadata and metadata.image or data.item),
header = metadata and metadata.label or Items[data.item].label,
}
for k in pairsByKeys(amounts) do
local settext = ""
local itemTable = {}
for l, b in pairs(data.craft[data.item]) do
itemTable[l] = (b * k)
settext = settext..(settext ~= "" and br or "")..Items[l].label..(b * k > 1 and " x"..b * k or "")
Wait(0)
end
local disable, stashname = checkHasItem(data.stashName, itemTable)
Menu[#Menu + 1] = {
isMenuHeader = not disable,
arrow = disable,
header = "Craft - x"..(k * data.craft.amount),
txt = settext,
onSelect = function()
makeItem({
item = data.item,
craft = data.craft,
craftable = data.craftable,
amount = k,
coords = data.coords,
stashName = stashname,
stashTable = data.stashName,
onBack = data.onBack,
metadata = data.metadata,
})
end,
}
end
openMenu(Menu, { header = data.craftable.Header, onBack = function() craftingMenu(data) end })
end
-------------------------------------------------------------
-- Crafting Process
-------------------------------------------------------------
--- Initiates the crafting process for a specified item.
---
--- Plays crafting animations, shows progress bars, removes ingredients, and triggers item creation.
---
--- @param data table Crafting configuration containing:
--- - item `string`) The item to craft.
--- - craft (`table`) The crafting recipe.
--- - craftable (`table`) Crafting options.
--- - amount (`number`) (optional): Quantity to craft (default 1).
--- - coords (`vector3`) where crafting occurs.
--- - stashName (`string`) The stash name(s) for item availability.
--- - onBack (`function`) Callback when returning.
--- - metadata (`table`) (optional): Metadata for the crafted item.
---
--- @usage
--- ```lua
--- makeItem({
--- item = "weapon_pistol",
--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
--- craftable = craftingOptions,
--- amount = 2,
--- coords = vector3(100,200,300),
--- stashName = "crafting_stash",
--- onBack = function() craftingMenu(data) end,
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
--- })
--- ```
function makeItem(data)
if CraftLock then return end
CraftLock = true
data.stashName = data.stashTable or data.stashName
local bartime = (data.craftable.progressBar and data.craftable.progressBar.time) or 5000
local bartext = (data.craftable.progressBar and data.craftable.progressBar.label)
or (Loc[Config.Lan].progressbar and Loc[Config.Lan].progressbar["progress_make"])
or "Making "
local animDict = (data.craftable.Anims and data.craftable.Anims.animDict) or "amb@prop_human_parking_meter@male@idle_a"
local anim = (data.craftable.Anims and data.craftable.Anims.anim) or "idle_a"
local craftAmount = (data.amount and data.amount ~= 1) and data.amount or 1
local metadata = data.metadata or nil
local prop = data.craftable.Anims and data.craftable.Anims.prop or nil
local canReturn = true
local crafted, crafting = true, true
local cam = createTempCam(PlayerPedId(), data.coords)
startTempCam(cam)
for i = 1, craftAmount do
for k, v in pairs(data.craft) do
local excludeKeys = {
amount = true, info = true, metadata = true, description = true,
job = true, gang = true, oneUse = true, slot = true,
blueprintRef = true, craftingLevel = true, craftedItems = true,
hasCrafted = true, exp = true, anim = true, time = true,
}
if not excludeKeys[k] then
if type(v) == "table" then
for l, b in pairs(v) do
if crafting and progressBar({
label = "Using "..b.." "..Items[l].label,
time = 1000,
cancel = true,
dict = 'pickup_object',
anim = "putdown_low",
flag = 48,
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 crafted then
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)
end
if crafting and progressBar({
label = bartext..((metadata and metadata.label) or Items[data.item].label),
time = bartime,
cancel = true,
dict = animDict,
anim = anim,
flag = 49,
icon = data.item,
}) then
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata)
CreateThread(function()
if data.craft["hasCrafted"] ~= nil then
debugPrint("hasCrafted Found, marking '"..data.item.."' as crafted for player")
data.craftable.craftedItems[data.item] = true
triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
end
Wait(100)
if data.craft["exp"] ~= nil then
craftingLevel += data.craft["exp"].give
jsonPrint(data.craft["exp"])
debugPrint("exp found, giving exp for '"..data.item.."'")
triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
end
end)
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)
canReturn = false
end
else
crafting = false
break
end
if craftProp then destroyProp(craftProp) end
end
end
end
end
Wait(500)
end
stopTempCam()
CraftLock = false
lockInv(false)
if canReturn then craftingMenu(data) end
ClearPedTasks(PlayerPedId())
end
-------------------------------------------------------------
-- Server Event Handler: Crafted Item
-------------------------------------------------------------
--- Server event handler for giving the crafted item to the player.
---
--- Removes required ingredients from the player's inventory or stash,
--- then adds the crafted item to their inventory.
---
--- @param ItemMake string The item being crafted.
--- @param craftable table The crafting recipe and details.
--- @param stashName string|table The stash name(s) to remove ingredients from.
--- @param metadata table (optional) Metadata for the crafted item.
RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, stashName, metadata)
local src = source
local hasItems, hasTable = hasItem(ItemMake, 1, src)
if stashName then
local itemRemove = {}
if type(stashName) == "table" then
for _, name in pairs(stashName) do
stashItems = getStash(name)
for k, v in pairs(craftable[ItemMake] or {}) do
for _, b in pairs(stashItems or {}) do
if k == b.name then itemRemove[k] = v end
end
end
end
else
stashItems = getStash(stashName)
for k, v in pairs(craftable[ItemMake] or {}) do
for _, b in pairs(stashItems or {}) do
if k == b.name then itemRemove[k] = v end
end
end
end
stashRemoveItem(stashItems, stashName, itemRemove)
else
if craftable then
for k, v in pairs(craftable[ItemMake] or {}) do
removeItem(tostring(k), v, src)
end
end
end
addItem(ItemMake, craftable.amount or 1, metadata, src)
-- Optionally, add experience here:
-- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end
end)
-------------------------------------------------------------
-- Selling Menu and Animation
-------------------------------------------------------------
--- Opens a selling menu with available items and prices.
---
--- @param data table Contains selling menu data:
--- - sellTable (`table`) Table with Header and Items (item names and prices).
--- - ped (optional) (`number`) Ped entity involved.
--- - onBack (optional) (`function`) Callback for returning.
--- @usage
--- ```lua
--- sellMenu({
--- sellTable = {
--- Header = "Sell Items",
--- Items = {
--- ["gold_ring"] = 100,
--- ["diamond"] = 500,
--- },
--- },
--- ped = pedEntity,
--- onBack = function() print("Returning to previous menu") end,
--- })
--- ```
function sellMenu(data)
local origData = data
local Menu = {}
if data.sellTable.Items then
local itemList = {}
for k, v in pairs(data.sellTable.Items) do itemList[k] = 1 end
local _, hasTable = hasItem(itemList)
for k, v in pairsByKeys(data.sellTable.Items) do
Menu[#Menu + 1] = {
isMenuHeader = not hasTable[k].hasItem,
icon = invImg(k),
header = Items[k].label..(hasTable[k].hasItem and "💰 (x"..hasTable[k].count..")" or ""),
txt = Loc[Config.Lan].info["sell_all"]..v.." "..Loc[Config.Lan].info["sell_each"],
onSelect = function()
sellAnim({ item = k, price = v, ped = data.ped, onBack = function() sellMenu(data) end })
end,
}
end
else
for k, v in pairsByKeys(data.sellTable) do
if type(v) == "table" then
Menu[#Menu + 1] = {
arrow = true,
header = k,
txt = "Amount of items: "..countTable(v.Items),
onSelect = function()
v.onBack = function() sellMenu(origData) end
v.sellTable = data.sellTable[k]
sellMenu(v)
end,
}
end
end
end
openMenu(Menu, {
header = data.sellTable.Header or "Amount of items: "..countTable(data.sellTable.Items),
headertxt = data.sellTable.Header and "Amount of items: "..countTable(data.sellTable.Items) or "",
canClose = true,
onBack = data.onBack,
})
end
--- Plays the selling animation and processes the sale transaction.
---
--- Checks if the player has the item, plays animations, triggers the server event for selling,
--- and then calls the onBack callback if provided.
---
--- @param data table Contains:
--- `- item: The item to sell.
--- `- price: Price per item.
--- `- ped (optional): Ped entity involved.
--- `- onBack (optional): Callback to call on completion.
---@usage
--- ```lua
--- sellAnim({
--- item = "gold_ring",
--- price = 100,
--- ped = pedEntity,
--- onBack = function() sellMenu(data) end,
--- })
--- ```
function sellAnim(data)
if not hasItem(data.item, 1) then
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error")
return
end
-- Remove any attached clipboard objects.
for _, obj in pairs(GetGamePool('CObject')) do
for _, model in pairs({ `p_cs_clipboard` }) do
if GetEntityModel(obj) == model and IsEntityAttachedToEntity(data.ped, obj) then
DeleteObject(obj)
DetachEntity(obj, 0, 0)
SetEntityAsMissionEntity(obj, true, true)
Wait(100)
DeleteEntity(obj)
end
end
end
TriggerServerEvent(getScript().."Sellitems", data)
lookEnt(data.ped)
local dict = "mp_common"
playAnim(dict, "givetake2_a", 0.3, 2)
playAnim(dict, "givetake2_b", 0.3, 2, data.ped)
Wait(2000)
StopAnimTask(PlayerPedId(), dict, "givetake2_a", 0.5)
StopAnimTask(data.ped, dict, "givetake2_b", 0.5)
if data.onBack then data.onBack() end
end
--- Server event handler for processing item sales.
--- Removes sold items from inventory and funds the player based on the sale.
RegisterNetEvent(getScript().."Sellitems", function(data)
local src = source
local hasItems, hasTable = hasItem(data.item, 1, src)
if hasItems then
removeItem(data.item, hasTable[data.item].count, src)
TriggerEvent(getScript()..":server:FundPlayer", (hasTable[data.item].count * data.price), "cash", src)
else
triggerNotify(nil, Loc[Config.Lan].error["dont_have"].." "..Items[data.item].label, "error", src)
end
end)
-------------------------------------------------------------
-- Shop Interface
-------------------------------------------------------------
--- Opens a shop interface for the player.
---
--- Checks job/gang restrictions, then uses the active inventory system to open the shop.
--- @param data table Contains:
--- - shop (`string`) The shop identifier.
--- - items (`table`) The items available in the shop.
--- - coords (`vector3`) where the shop is located.
--- - job/gang (optional) (`string`) Job or gang requirements.
---@usage
--- ```lua
--- openShop({
--- shop = "weapon_shop",
--- items = weaponShopItems,
--- coords = vector3(100.0, 200.0, 300.0),
--- job = "police",
--- })
--- ```
function openShop(data)
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
if isStarted(OXInv) then
exports[OXInv]:openInventory('shop', { type = data.shop })
elseif isStarted(QBInv) then
if QBInvNew then
TriggerServerEvent(getScript()..':server:OpenShopNewQB', data.shop)
else
TriggerServerEvent(Config.General.JimShops and "jim-shops:ShopOpen" or "inventory:server:OpenInventory", "shop", data.items.label, data.items)
end
--elseif isStarted(OrigenInv) then -- Needs testing, not sure if i did this right
-- exports[OrigenInv]:openInventory('shop', data.shop, data.items)
else
TriggerServerEvent(Config.General.JimShops and "jim-shops:ShopOpen" or "inventory:server:OpenInventory", "shop", data.items.label, data.items)
end
lookEnt(data.coords)
end
--- Server event handler for opening a shop using the new QB inventory system.
RegisterNetEvent(getScript()..':server:OpenShopNewQB', function(data)
exports[QBInv]:OpenShop(source, data)
end)
-------------------------------------------------------------
-- Server Callback Registration
-------------------------------------------------------------
if isServer() then
createCallback(getScript()..':server:canCarry', function(source, itemTable) local result = canCarry(itemTable, source) return result end)
end