├── .gitignore ├── web ├── src │ ├── styles │ │ └── .gitkeep │ ├── components │ │ ├── .gitkeep │ │ ├── Crafting │ │ │ ├── Button │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Item │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Blueprint │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ ├── Info │ │ │ ├── Queue │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Required │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Item │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── Bottom │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── index.module.css │ │ │ └── index.tsx │ │ └── Popup │ │ │ ├── index.module.css │ │ │ └── index.tsx │ ├── vite-env.d.ts │ ├── types │ │ ├── craft.ts │ │ ├── queue.ts │ │ └── crafting.ts │ ├── assests │ │ └── seguibl.ttf │ ├── utils │ │ ├── checkFave.ts │ │ ├── misc.ts │ │ ├── sendNui.ts │ │ ├── getThemeVariables.ts │ │ ├── debugData.ts │ │ ├── fetchNui.ts │ │ └── updateRipples.ts │ ├── index.module.css │ ├── main.tsx │ ├── store │ │ ├── stores │ │ │ ├── config │ │ │ │ └── config.ts │ │ │ ├── crafting │ │ │ │ ├── ui.ts │ │ │ │ ├── queue.ts │ │ │ │ └── crafting.ts │ │ │ └── popup │ │ │ │ └── popup.ts │ │ └── store.ts │ ├── hooks │ │ └── useNuiEvent.ts │ ├── style.css │ ├── providers │ │ └── VisibilityProvider.tsx │ └── App.tsx ├── build │ ├── assets │ │ ├── seguibl.4098759b.ttf │ │ ├── fa-brands-400.5656d596.ttf │ │ ├── fa-solid-900.fbbf06d7.ttf │ │ ├── fa-brands-400.3a8924cd.woff2 │ │ ├── fa-regular-400.5d02dc9b.ttf │ │ ├── fa-solid-900.9fc85f3a.woff2 │ │ ├── fa-regular-400.2bccecf0.woff2 │ │ ├── fa-v4compatibility.09663a36.ttf │ │ ├── fa-v4compatibility.4d4a2d7f.woff2 │ │ ├── index.c1bfc35e.css │ │ └── index.f5b612d0.js │ └── index.html ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tsconfig.json └── package.json ├── client ├── target │ ├── standalone.lua │ ├── qb.lua │ └── ox.lua ├── framework │ ├── notify.lua │ ├── customChecks.lua │ ├── qbox │ │ └── events.lua │ ├── standalone │ │ └── events.lua │ ├── esx │ │ └── events.lua │ └── qbcore │ │ └── events.lua ├── nuiFunctions.lua ├── blueprints │ └── events.lua ├── raycast.lua ├── open.lua ├── benches │ ├── objects.lua │ ├── setup.lua │ └── placing.lua ├── events.lua ├── nuiCallbacks.lua └── zones │ └── zones.lua ├── INSTALLFILES ├── images │ ├── pure_bench.png │ └── pure_blueprint.png └── sql │ └── crafting.sql ├── server ├── framework │ ├── notify.lua │ ├── customChecks.lua │ ├── qbcore │ │ └── functions.lua │ ├── qbox │ │ └── functions.lua │ ├── esx │ │ └── functions.lua │ └── standalone │ │ └── functions.lua ├── craft │ ├── craft.lua │ ├── claimCraft.lua │ └── craftItem.lua ├── modules │ ├── callbacks.lua │ ├── faves.lua │ ├── items.lua │ └── events.lua ├── inventory │ ├── esxInventory.lua │ ├── psInventory.lua │ ├── qsInventory.lua │ ├── qbInventory.lua │ └── oxInventory.lua ├── queue.lua ├── benches │ ├── placing.lua │ └── initiate.lua └── blueprints │ └── blueprints.lua ├── locales ├── locales.lua └── translations │ └── en.lua ├── README.md ├── config ├── themes.json ├── config.lua ├── item_blueprints.lua └── items.lua ├── fxmanifest.lua └── shared └── functions.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /web/src/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/target/standalone.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/types/craft.ts: -------------------------------------------------------------------------------- 1 | export interface craft { 2 | amount: number; 3 | } 4 | -------------------------------------------------------------------------------- /web/src/assests/seguibl.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/src/assests/seguibl.ttf -------------------------------------------------------------------------------- /client/framework/notify.lua: -------------------------------------------------------------------------------- 1 | function notifySystem(table) 2 | if not table then return end 3 | lib.notify(table) 4 | end -------------------------------------------------------------------------------- /INSTALLFILES/images/pure_bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/INSTALLFILES/images/pure_bench.png -------------------------------------------------------------------------------- /INSTALLFILES/images/pure_blueprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/INSTALLFILES/images/pure_blueprint.png -------------------------------------------------------------------------------- /web/build/assets/seguibl.4098759b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/seguibl.4098759b.ttf -------------------------------------------------------------------------------- /web/src/utils/checkFave.ts: -------------------------------------------------------------------------------- 1 | export const checkFave = (itemName: string, faves: any) => { 2 | return faves[itemName] ? true : false; 3 | }; 4 | -------------------------------------------------------------------------------- /web/build/assets/fa-brands-400.5656d596.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-brands-400.5656d596.ttf -------------------------------------------------------------------------------- /web/build/assets/fa-solid-900.fbbf06d7.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-solid-900.fbbf06d7.ttf -------------------------------------------------------------------------------- /web/build/assets/fa-brands-400.3a8924cd.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-brands-400.3a8924cd.woff2 -------------------------------------------------------------------------------- /web/build/assets/fa-regular-400.5d02dc9b.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-regular-400.5d02dc9b.ttf -------------------------------------------------------------------------------- /web/build/assets/fa-solid-900.9fc85f3a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-solid-900.9fc85f3a.woff2 -------------------------------------------------------------------------------- /web/build/assets/fa-regular-400.2bccecf0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-regular-400.2bccecf0.woff2 -------------------------------------------------------------------------------- /web/build/assets/fa-v4compatibility.09663a36.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-v4compatibility.09663a36.ttf -------------------------------------------------------------------------------- /web/build/assets/fa-v4compatibility.4d4a2d7f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/purescripts-fivem/pure-crafting/HEAD/web/build/assets/fa-v4compatibility.4d4a2d7f.woff2 -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /server/framework/notify.lua: -------------------------------------------------------------------------------- 1 | function notifySystem(source, table) 2 | -- Change this function to add your own notify system 3 | if not table then return end 4 | if not source then return end 5 | 6 | lib.notify(source, table) 7 | end -------------------------------------------------------------------------------- /client/framework/customChecks.lua: -------------------------------------------------------------------------------- 1 | function customChecks(source) 2 | -- these are in place so you can add your own checks before a user can place a crafting bench 3 | -- return false to not allow them to place the bench 4 | return true 5 | end -------------------------------------------------------------------------------- /server/framework/customChecks.lua: -------------------------------------------------------------------------------- 1 | function customChecks(source) 2 | -- these are in place so you can add your own checks before a user can place a crafting bench 3 | -- return false to not allow them to place the bench 4 | return true 5 | end -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: './', 8 | build: { 9 | outDir: 'build', 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /client/nuiFunctions.lua: -------------------------------------------------------------------------------- 1 | function toggleNuiFrame(shouldShow) 2 | SetNuiFocus(shouldShow, shouldShow) 3 | SendReactMessage('setVisible', shouldShow) 4 | end 5 | 6 | function SendReactMessage(action, data) 7 | SendNUIMessage({ 8 | action = action, 9 | data = data 10 | }) 11 | end -------------------------------------------------------------------------------- /web/src/types/queue.ts: -------------------------------------------------------------------------------- 1 | export interface queue { 2 | items: item[]; 3 | finished: item[]; 4 | } 5 | 6 | export interface item { 7 | image: string; 8 | itemName: string; 9 | secondsLeft: number; 10 | timeStarted: number; 11 | timeToCraft: number; 12 | id: number; 13 | } 14 | -------------------------------------------------------------------------------- /web/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | export const isEnvBrowser = (): boolean => !(window as any).invokeNative; 2 | 3 | export const getResourceName = (): string => { 4 | if (isEnvBrowser()) return 'clothing'; 5 | return (window as any).GetParentResourceName(); 6 | }; 7 | 8 | export const noop = () => {}; 9 | -------------------------------------------------------------------------------- /server/framework/qbcore/functions.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'qbcore' then return end 2 | 3 | QBCore = exports['qb-core']:GetCoreObject() 4 | 5 | function getPlayerUniqueId(source) 6 | local player = QBCore.Functions.GetPlayer(source) 7 | if not player then return end 8 | return player.PlayerData.citizenid 9 | end -------------------------------------------------------------------------------- /web/src/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | display: flex; 4 | flex-direction: row; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100vh; 8 | width: 100%; 9 | background-size: cover; 10 | background-position: center; 11 | background-repeat: no-repeat; 12 | gap: 1vw; 13 | } 14 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | # build 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NUI React Boilerplate 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/framework/qbox/functions.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'qbox' then return end 2 | 3 | function getPlayerUniqueId(source) 4 | local player = exports.qbx_core:GetPlayer(source) 5 | if not player then return end 6 | if not player.PlayerData.citizenid then return end 7 | debugPrint('getPlayerUniqueId', player.PlayerData.citizenid) 8 | return player.PlayerData.citizenid 9 | end -------------------------------------------------------------------------------- /server/framework/esx/functions.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'esx' then return end 2 | 3 | ESX = exports['es_extended']:getSharedObject() 4 | 5 | function getPlayerUniqueId(source) 6 | local player = ESX.GetPlayerFromId(source) 7 | if not player then return end 8 | if not player.identifier then return end 9 | debugPrint('getPlayerUniqueId', player.identifier) 10 | return player.identifier 11 | end -------------------------------------------------------------------------------- /locales/locales.lua: -------------------------------------------------------------------------------- 1 | Language = {} 2 | 3 | function _Lang(key) 4 | local lang = Config.language 5 | if not Language[lang] then 6 | lang = 'en' 7 | end 8 | local value = Language[lang] 9 | for k in key:gmatch("[^.]+") do 10 | if not value then 11 | debugPrint("Missing locale for: " .. key) 12 | return "" 13 | end 14 | value = value[k] 15 | end 16 | return value 17 | end -------------------------------------------------------------------------------- /server/framework/standalone/functions.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'standalone' then return end 2 | 3 | function getPlayerUniqueId(source) 4 | -- local player = exports.qbx_core:GetPlayer(source) 5 | -- if not player then return end 6 | -- if not player.PlayerData.citizenid then return end 7 | -- debugPrint('getPlayerUniqueId', player.PlayerData.citizenid) 8 | -- return player.PlayerData.citizenid 9 | -- Return their uniqueId 10 | end -------------------------------------------------------------------------------- /web/src/components/Crafting/Button/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 2.6vw; 3 | height: 100%; 4 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.35)); 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-shrink: 0; 9 | cursor: pointer; 10 | overflow: hidden; 11 | white-space: nowrap; 12 | } 13 | 14 | .icon { 15 | font-size: 1.1vw; 16 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.45)); 17 | } 18 | -------------------------------------------------------------------------------- /client/blueprints/events.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent('pure-crafting:useBlueprint', function(name) 2 | if not currentZone then 3 | notifySystem({ 4 | title = _Lang('blueprints.notAtABench'), 5 | type = 'error', 6 | position = Config.libText.notfiyPoistion, 7 | }) 8 | return 9 | end 10 | local benchId = currentZone.benchId 11 | TriggerServerEvent('pure-crafting:blueprintUsed', benchId, name) 12 | end) -------------------------------------------------------------------------------- /client/framework/qbox/events.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'qbox' then return end 2 | 3 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 4 | TriggerServerEvent('pure-crafting:playerLoaded') 5 | end) 6 | 7 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 8 | TriggerServerEvent('pure-crafting:playerUnloaded') 9 | end) 10 | 11 | function getPlayerUniqueId() 12 | local citizenid = QBX.PlayerData.citizenid 13 | return citizenid 14 | end -------------------------------------------------------------------------------- /client/framework/standalone/events.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'standalone' then return end 2 | 3 | -- onPlayerLoad event here 4 | RegisterNetEvent('', function() 5 | TriggerServerEvent('pure-crafting:playerLoaded') 6 | end) 7 | 8 | -- onPlayerUnload event here 9 | RegisterNetEvent('', function() 10 | TriggerServerEvent('pure-crafting:playerUnloaded') 11 | end) 12 | 13 | function getPlayerUniqueId() 14 | local citizenid = '' 15 | return citizenid 16 | end -------------------------------------------------------------------------------- /web/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NUI React Boilerplate 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web/src/utils/sendNui.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser } from './misc'; 2 | import { getResourceName } from './misc'; 3 | 4 | export const sendNui = (eventName: string, data?: any) => { 5 | if (isEnvBrowser()) return; 6 | const resourceName = getResourceName(); 7 | const xhr = new XMLHttpRequest(); 8 | xhr.open('POST', `https://${resourceName}/${eventName}`, true); 9 | xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 10 | xhr.send(JSON.stringify(data)); 11 | }; 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crafting-source 2 | 3 | Discord: https://discord.gg/fWKYUcgjgB 4 | 5 | Read the docs @ docs.purescripts.net 6 | 7 | Simple installation: 8 | 9 | - Install dependencies: 10 | - ox_mysql 11 | - ox_lib 12 | 13 | - Ensure script after these in server cfg 14 | - Add images from /INSTALLFILES/images to your selected inventory 15 | - Add items from server/inventory/_yourInventory_.lua (at the bottom of the file) 16 | - Add sql from INSTALLFILES/sql to your database 17 | - Start server 18 | -------------------------------------------------------------------------------- /client/target/qb.lua: -------------------------------------------------------------------------------- 1 | if Config.targetingOptions.target ~= 'qb' then return end 2 | 3 | function addTargetToCoords(coords, boxSize, table, name) 4 | exports['qb-target']:AddBoxZone(name, coords, boxSize.x, boxSize.y, { 5 | name = name, 6 | debugPoly = Config.debug, 7 | minZ = coords.z - 2, 8 | maxZ = coords.z + 2, 9 | heading = coords.w 10 | }, table) 11 | end 12 | 13 | function removeZone(name) 14 | exports['qb-target']:RemoveZone(name) 15 | end -------------------------------------------------------------------------------- /client/framework/esx/events.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'esx' then return end 2 | 3 | local ESX = exports['es_extended']:getSharedObject() 4 | 5 | RegisterNetEvent('esx:playerLoaded', function() 6 | TriggerServerEvent('pure-crafting:playerLoaded') 7 | end) 8 | 9 | RegisterNetEvent('esx:onPlayerLogout', function() 10 | TriggerServerEvent('pure-crafting:playerUnloaded') 11 | end) 12 | 13 | function getPlayerUniqueId(source) 14 | local PlayerData = ESX.GetPlayerData() 15 | return PlayerData.identifier 16 | end 17 | -------------------------------------------------------------------------------- /client/framework/qbcore/events.lua: -------------------------------------------------------------------------------- 1 | if Config.framework ~= 'qbcore' then return end 2 | 3 | local QBCore = exports['qb-core']:GetCoreObject() 4 | 5 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 6 | TriggerServerEvent('pure-crafting:playerLoaded') 7 | end) 8 | 9 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 10 | TriggerServerEvent('pure-crafting:playerUnloaded') 11 | end) 12 | 13 | function getPlayerUniqueId() 14 | local citizenid = QBCore.Functions.GetPlayerData().citizenid 15 | return citizenid 16 | end -------------------------------------------------------------------------------- /web/src/utils/getThemeVariables.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser, getResourceName } from './misc'; 2 | import defaultConfig from '../../../config/themes.json'; 3 | 4 | export const getThemeVariables = async (): Promise => { 5 | if (isEnvBrowser()) { 6 | console.log('Using default config'); 7 | return defaultConfig; 8 | } 9 | 10 | const resourceName = getResourceName(); 11 | const config = await fetch( 12 | `https://cfx-nui-${resourceName}/config/themes.json` 13 | ).then((res) => res.json()); 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /web/src/components/Info/Queue/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 4.8vw; 3 | height: 8.2vh; 4 | flex-shrink: 0; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | position: relative; 9 | cursor: pointer; 10 | transition: 0.3s; 11 | border-radius: 1px; 12 | } 13 | 14 | .img { 15 | width: 3.4vw; 16 | height: 3.4vw; 17 | } 18 | 19 | .text { 20 | position: absolute; 21 | font-size: 1vw; 22 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 23 | 0px 4px 7px rgba(0, 0, 0, 0.25); 24 | } 25 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { VisibilityProvider } from './providers/VisibilityProvider'; 4 | import { Provider } from 'react-redux'; 5 | import { boilerplateStore } from './store/store'; 6 | import App from './App'; 7 | import './style.css'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')!).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | 'no-unused-vars': 'off', 18 | '@typescript-eslint/no-unused-vars': 'error', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /config/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "#303030", 3 | "border": "#1F1F1F", 4 | "white": "#FFFFFF", 5 | "blueprintGray": "#494949", 6 | "green": "#89BA33", 7 | "gray": "#949494", 8 | "red": "#A14050", 9 | "blue": "#3376BA", 10 | "greenFaded": "#39463E", 11 | "redFaded": "#3D2525", 12 | "button": "#242424", 13 | "popup": { 14 | "background": "#303030", 15 | "border": "#2C2C2C", 16 | "text": "#B2B2B2", 17 | "redBackground": "#6C1A1A", 18 | "redBorder": "#8C1F1F", 19 | "redText": "#D11616", 20 | "greenBackground": "#1A6C20", 21 | "greenBorder": "#1F8C26", 22 | "greenText": "#16CD21" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/src/utils/debugData.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser } from './misc'; 2 | 3 | interface DebugEvent { 4 | action: string; 5 | data: T; 6 | } 7 | 8 | export const debugData =

(events: DebugEvent

[], timer = 1000): void => { 9 | if (import.meta.env.MODE === 'development' && isEnvBrowser()) { 10 | for (const event of events) { 11 | setTimeout(() => { 12 | window.dispatchEvent( 13 | new MessageEvent('message', { 14 | data: { 15 | action: event.action, 16 | data: event.data, 17 | }, 18 | }), 19 | ); 20 | }, timer); 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/utils/fetchNui.ts: -------------------------------------------------------------------------------- 1 | import { isEnvBrowser } from "./misc"; 2 | 3 | export async function fetchNui(eventName: string, data?: any, mockData?: T): Promise { 4 | const options = { 5 | method: 'post', 6 | headers: { 7 | 'Content-Type': 'application/json; charset=UTF-8', 8 | }, 9 | body: JSON.stringify(data), 10 | }; 11 | 12 | if (isEnvBrowser() && mockData) return mockData; 13 | 14 | const resourceName = (window as any).GetParentResourceName ? (window as any).GetParentResourceName() : 'nui-frame-app'; 15 | 16 | const resp = await fetch(`https://${resourceName}/${eventName}`, options); 17 | 18 | const respFormatted = await resp.json() 19 | 20 | return respFormatted 21 | } 22 | -------------------------------------------------------------------------------- /web/src/types/crafting.ts: -------------------------------------------------------------------------------- 1 | export interface craftingState { 2 | // categories: any; 3 | // [key: string]: craftingItem[]; 4 | 5 | items: craftingItem[]; 6 | blueprints: craftingItem[]; 7 | selectedItem: number; 8 | currentItem: craftingItem | null; 9 | amount: number; 10 | } 11 | 12 | export interface item { 13 | itemName: string; 14 | name: string; 15 | amount: number; 16 | myAmount: number; 17 | image: string; 18 | } 19 | 20 | export interface craftingItem { 21 | itemName: string; // 22 | name: string; // 23 | image: string; // 24 | category?: string; // 25 | id: number; // 26 | description: string; // 27 | craftingTime: number; // 28 | uses?: number; 29 | requiredItems: item[]; 30 | type?: string; 31 | } 32 | -------------------------------------------------------------------------------- /INSTALLFILES/sql/crafting.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `crafting_benches` ( 2 | `id` int(11) NOT NULL AUTO_INCREMENT, 3 | `location` varchar(255) NOT NULL, 4 | `rotation` varchar(255) NOT NULL, 5 | `queue` longtext NOT NULL DEFAULT '[]', 6 | `finished` longtext NOT NULL DEFAULT '[]', 7 | `blueprints` longtext NOT NULL DEFAULT '[]', 8 | `type` varchar(255) DEFAULT NULL, 9 | `userPlaced` varchar(255) DEFAULT NULL, 10 | PRIMARY KEY (`id`) 11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; 12 | 13 | CREATE TABLE IF NOT EXISTS `crafting_users` ( 14 | `uniqueId` varchar(255) NOT NULL, 15 | `faves` longtext NOT NULL DEFAULT '{}', 16 | `amountPlaced` int(11) NOT NULL DEFAULT 0 17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; 18 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", ".eslintrc.cjs"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /client/raycast.lua: -------------------------------------------------------------------------------- 1 | function raycast() 2 | local coords, normal = GetWorldCoordFromScreenCoord(0.5, 0.5) 3 | local destination = coords + normal * (distance or 10) 4 | local handle = StartShapeTestLosProbe(coords.x, coords.y, coords.z, destination.x, destination.y, destination.z, 5 | flags or 511, cache.ped, ignore or 4) 6 | 7 | while true do 8 | Wait(0) 9 | local retval, hit, endCoords, surfaceNormal, materialHash, entityHit = GetShapeTestResultIncludingMaterial(handle) 10 | 11 | if retval ~= 1 then 12 | return hit, entityHit, endCoords, surfaceNormal, materialHash 13 | end 14 | end 15 | end 16 | 17 | function normalToRotation(normal) 18 | local pitch = -math.asin(normal.y) * (180.0 / math.pi) 19 | local yaw = math.atan(normal.x, normal.z) * (180.0 / math.pi) 20 | return vec(pitch, yaw, 0.0) 21 | end -------------------------------------------------------------------------------- /server/craft/craft.lua: -------------------------------------------------------------------------------- 1 | function Queue:finishedCraft(craftedItem) 2 | local item = self.items[1] 3 | if item.name ~= craftedItem.name then 4 | debugPrint('Queue:finishedCraft | failed - item names dont match', json.encode(item), json.encode(craftedItem)) 5 | return 6 | end 7 | table.remove(self.items, 1) 8 | self.finished[#self.finished + 1] = item 9 | if self.items[1] then 10 | self.items[1].timeStarted = os.time() 11 | end 12 | debugPrint('Queue:finishedCraft | ', json.encode(item), json.encode(self.items), json.encode(self.finished)) 13 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET queue = ?, finished = ? WHERE id = ?', { 14 | json.encode(self.items), json.encode(self.finished), self.benchId 15 | }) 16 | self:triggerEvent('pure-crafting:finishedCraft', self.items, self.finished) 17 | return true 18 | end -------------------------------------------------------------------------------- /web/src/store/stores/config/config.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface configState { 4 | config: any; 5 | language: any; 6 | theme: any; 7 | } 8 | 9 | const initialState: configState = { 10 | config: [], 11 | language: [], 12 | theme: [], 13 | }; 14 | 15 | export const configSlice = createSlice({ 16 | name: 'config', 17 | initialState, 18 | reducers: { 19 | setConfig: (state, action: PayloadAction) => { 20 | state.config = action.payload; 21 | }, 22 | setLanguage: (state, action: PayloadAction) => { 23 | state.language = action.payload; 24 | }, 25 | setTheme: (state, action: PayloadAction) => { 26 | state.theme = action.payload; 27 | }, 28 | }, 29 | }); 30 | 31 | export default configSlice.reducer; 32 | export const { setConfig, setLanguage, setTheme } = configSlice.actions; 33 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version "cerulean" 2 | 3 | description "Pure Crafting" 4 | author "purescripts.net" 5 | version '1.0.0' 6 | 7 | lua54 'yes' 8 | 9 | game 'gta5' 10 | 11 | ui_page 'web/build/index.html' 12 | 13 | client_scripts { 14 | 'client/**/*', 15 | -- '@qbx_core/modules/playerdata.lua', -- UNCOMMENT THESE IF YOU USE QBOX 16 | } 17 | 18 | server_scripts { 19 | "@oxmysql/lib/MySQL.lua", -- for oxmysql 20 | 'server/queue.lua', 21 | 'server/framework/**/*', 22 | 'server/modules/**/*', 23 | 'server/craft/**/*', 24 | 'server/benches/**/*', 25 | 'server/inventory/**/*', 26 | 'server/blueprints/**/*', 27 | } 28 | 29 | shared_scripts { 30 | '@ox_lib/init.lua', -- for oxlib 31 | 'config/*.lua', 32 | 'shared/**/*', 33 | 'locales/**/*', 34 | } 35 | 36 | files { 37 | 'web/build/index.html', 38 | 'web/build/**/*', 39 | 'config/themes.json' 40 | } 41 | 42 | exports { 43 | 'placeBench', 44 | } -------------------------------------------------------------------------------- /web/src/store/stores/crafting/ui.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | categories: [ 5 | { 6 | icon: 'fa-star', 7 | category: 'fave', 8 | }, 9 | { 10 | icon: 'fa-burger', 11 | category: 'burger', 12 | }, 13 | { 14 | icon: 'fa-gun', 15 | category: 'guns', 16 | }, 17 | ], 18 | faves: { 19 | assualtrifle: true, 20 | }, 21 | }; 22 | 23 | export const uiSlice = createSlice({ 24 | name: 'ui', 25 | initialState, 26 | reducers: { 27 | setCategories: (state, action: PayloadAction) => { 28 | state.categories = action.payload; 29 | }, 30 | setFaves: (state, action: PayloadAction) => { 31 | state.faves = action.payload; 32 | }, 33 | }, 34 | }); 35 | 36 | export default uiSlice.reducer; 37 | export const { setCategories, setFaves } = uiSlice.actions; 38 | -------------------------------------------------------------------------------- /client/target/ox.lua: -------------------------------------------------------------------------------- 1 | if Config.targetingOptions.target ~= 'ox' then return end 2 | 3 | local createdZones = {} 4 | 5 | function oxConvertOptions(options, distance) 6 | for k,v in pairs(options) do 7 | v.name = v.label 8 | v.onSelect = v.action 9 | v.distance = distance 10 | v.groups = v.job or v.gang 11 | v.canInteract = v.canInteract or function() return true end 12 | end 13 | return options 14 | end 15 | 16 | function addTargetToCoords(coords, boxSize, table, name) 17 | local rotation = table.rotation or 0.0 18 | createdZones[name] = exports['ox_target']:addBoxZone({ 19 | coords = coords, 20 | size = boxSize, 21 | rotation = rotation, 22 | debug = Config.Debug, 23 | options = oxConvertOptions(table.options, table.distance) 24 | }) 25 | end 26 | 27 | function removeZone(name) 28 | exports['ox_target']:removeZone(createdZones[name]) 29 | end -------------------------------------------------------------------------------- /client/open.lua: -------------------------------------------------------------------------------- 1 | local isFirstTime = true 2 | 3 | RegisterNetEvent('pure-crafting:openCrafting', function() 4 | local src = source 5 | openCrafting(src) 6 | end) 7 | 8 | function openCrafting(source, newBenchId) 9 | local benchId = nil 10 | if Config.targetingOptions.interaction == 'interaction' then 11 | if not currentZone then return end 12 | benchId = currentZone.benchId 13 | else 14 | benchId = newBenchId 15 | end 16 | local items = lib.callback.await('pure-crafting:getItems', false, benchId) 17 | local data = lib.callback.await('pure-crafting:getData', false, benchId) 18 | SendReactMessage('itemsChange', items) 19 | SendReactMessage('updateItems', data.queue) 20 | SendReactMessage('updateFinished', data.finished) 21 | SendReactMessage('blueprints', data.blueprints) 22 | toggleNuiFrame(true) 23 | debugPrint('nuiCallback | Show NUI frame') 24 | TriggerScreenblurFadeIn(250) 25 | end -------------------------------------------------------------------------------- /web/src/components/Info/Required/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .text { 10 | font-size: 1.5vw; 11 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 12 | 0px 4px 7px rgba(0, 0, 0, 0.25); 13 | font-family: 'Black'; 14 | position: absolute; 15 | margin-top: -20vh; 16 | margin-left: -17.5vw; 17 | } 18 | 19 | .text2 { 20 | font-size: 1.5vw; 21 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 22 | 0px 4px 7px rgba(0, 0, 0, 0.25); 23 | font-family: 'Black'; 24 | } 25 | 26 | .housing { 27 | margin-top: 3vh; 28 | width: 85%; 29 | height: 65%; 30 | display: flex; 31 | justify-content: flex-start; 32 | align-items: center; 33 | flex-direction: row; 34 | gap: 3.5vw; 35 | overflow-x: hidden; 36 | overflow-y: hidden; 37 | scrollbar-width: none; 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/Info/Item/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 8vw; 3 | height: 16vh; 4 | display: flex; 5 | flex-direction: column; 6 | gap: 0.5vh; 7 | flex-shrink: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | .housing { 12 | width: 8vw; 13 | height: 15vh; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | position: relative; 18 | filter: drop-shadow(0px 0px 8px rgba(0, 0, 0, 0.15)); 19 | border-radius: 1px; 20 | } 21 | 22 | .imgText { 23 | font-size: 1vw; 24 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.3), -3px 0px 7px rgba(0, 0, 0, 0.3), 25 | 0px 4px 7px rgba(0, 0, 0, 0.3); 26 | position: absolute; 27 | top: 9.3vh; 28 | right: 0.5vh; 29 | } 30 | 31 | .text { 32 | width: 100%; 33 | height: 2.5vh; 34 | text-align: center; 35 | font-size: 1.05vw; 36 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 37 | 0px 4px 7px rgba(0, 0, 0, 0.25); 38 | } 39 | 40 | .img { 41 | position: absolute; 42 | width: 5.5vw; 43 | } 44 | -------------------------------------------------------------------------------- /web/src/components/Crafting/Item/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 7vw; 3 | height: 12.7vh; 4 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.35)); 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | cursor: pointer; 10 | overflow: hidden; 11 | } 12 | 13 | .fave { 14 | position: absolute; 15 | font-size: 0.75vw; 16 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.45)); 17 | margin-top: -10vh; 18 | margin-left: 5.3vw; 19 | } 20 | 21 | .icon { 22 | padding-bottom: 1vh; 23 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.35)); 24 | width: 7vh; 25 | height: 7vh; 26 | } 27 | 28 | .text { 29 | font-size: 0.9vw; 30 | position: absolute; 31 | margin-top: 9vh; 32 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 33 | 0px 4px 7px rgba(0, 0, 0, 0.25); 34 | width: 8.5vw; 35 | padding: 0 0.8vw; 36 | overflow: hidden; 37 | text-align: center; 38 | white-space: nowrap; 39 | text-overflow: ellipsis; 40 | } 41 | -------------------------------------------------------------------------------- /web/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; 3 | import { configSlice } from './stores/config/config'; 4 | import { craftingSlice } from './stores/crafting/crafting'; 5 | import { uiSlice } from './stores/crafting/ui'; 6 | import { queueSlice } from './stores/crafting/queue'; 7 | import { popupSlice } from './stores/popup/popup'; 8 | 9 | export const boilerplateStore = configureStore({ 10 | reducer: { 11 | config: configSlice.reducer, 12 | crafting: craftingSlice.reducer, 13 | queue: queueSlice.reducer, 14 | ui: uiSlice.reducer, 15 | popup: popupSlice.reducer, 16 | }, 17 | middleware: (getDefaultMiddleware) => 18 | getDefaultMiddleware({ 19 | serializableCheck: false, 20 | }), 21 | }); 22 | 23 | export const useAppDistpatch: () => typeof boilerplateStore.dispatch = 24 | useDispatch; 25 | export const useAppSelector: TypedUseSelectorHook< 26 | ReturnType 27 | > = useSelector; 28 | -------------------------------------------------------------------------------- /web/src/components/Crafting/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useAppSelector } from '../../../store/store'; 3 | import style from './index.module.css'; 4 | import '@fortawesome/fontawesome-free/css/all.min.css'; 5 | import updateRipples from '../../../utils/updateRipples'; 6 | 7 | interface Props { 8 | onClick: () => void; 9 | icon: string; 10 | } 11 | 12 | const Button = (props: Props) => { 13 | const theme = useAppSelector((state) => state.config.theme); 14 | 15 | useEffect(() => { 16 | updateRipples(); 17 | }); 18 | return ( 19 |

{ 26 | props.onClick(); 27 | }} 28 | id='ripple-animation'> 29 | 34 |
35 | ); 36 | }; 37 | 38 | export default Button; 39 | -------------------------------------------------------------------------------- /web/src/utils/updateRipples.ts: -------------------------------------------------------------------------------- 1 | const updateRipples = () => { 2 | const buttonRipple: any = document.querySelectorAll('#ripple-animation'); 3 | 4 | buttonRipple.forEach((button: any) => { 5 | button.onclick = ({ 6 | pageX, 7 | pageY, 8 | currentTarget, 9 | }: { 10 | pageX: number; 11 | pageY: number; 12 | currentTarget: any; 13 | }) => { 14 | let x = pageX - currentTarget.offsetLeft; 15 | let y = pageY - currentTarget.offsetTop; 16 | if (currentTarget.classList.contains('center-ripple')) { 17 | x = currentTarget.offsetWidth / 2; 18 | y = currentTarget.offsetHeight / 2; 19 | } 20 | const ripple = document.createElement('span'); 21 | // make the style of the ripple with the overflow hidden 22 | ripple.classList.add('ripple-effect'); 23 | ripple.style.left = `${x}px`; 24 | ripple.style.top = `${y}px`; 25 | button.appendChild(ripple); 26 | setTimeout(() => { 27 | ripple.remove(); 28 | }, 600); 29 | }; 30 | }); 31 | }; 32 | 33 | export default updateRipples; 34 | -------------------------------------------------------------------------------- /client/benches/objects.lua: -------------------------------------------------------------------------------- 1 | function deleteAllBenches() 2 | for i = 1, #Benches do 3 | local benchObj = Benches[i].obj 4 | if not benchObj or not DoesEntityExist(benchObj) then goto continue end 5 | DeleteEntity(benchObj) 6 | Benches[i].obj = nil 7 | ::continue:: 8 | end 9 | end 10 | 11 | function pickupBench(source, id) 12 | local result = lib.callback.await('pure-crafting:pickupBench', false, id) 13 | end 14 | 15 | function createBench(location, rotation, type) 16 | local model = generateObjFromType(type) 17 | lib.requestModel(model) 18 | -- RequestModel(model) 19 | -- while not HasModelLoaded(model) do 20 | -- print('waiting for model to load') 21 | -- Wait(0) 22 | -- end 23 | Wait(350) 24 | local object = CreateObject(model, location.x, location.y, location.z, false, false, false) 25 | SetEntityRotation(object, rotation.x, rotation.y, rotation.z, 1) 26 | PlaceObjectOnGroundProperly(object) 27 | -- FreezeEntityPosition(object, true) 28 | SetModelAsNoLongerNeeded(model) 29 | SetEntityCanBeDamaged(object, false) 30 | return object 31 | end -------------------------------------------------------------------------------- /web/src/hooks/useNuiEvent.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef } from 'react'; 2 | import { noop } from '../utils/misc'; 3 | 4 | interface NuiMessageData { 5 | action: string; 6 | data: T; 7 | } 8 | 9 | type NuiHandlerSignature = (data: T) => void; 10 | 11 | export const useNuiEvent = ( 12 | action: string, 13 | handler: (data: T) => void 14 | ) => { 15 | const savedHandler: MutableRefObject> = useRef(noop); 16 | 17 | // Make sure we handle for a reactive handler 18 | useEffect(() => { 19 | savedHandler.current = handler; 20 | }, [handler]); 21 | 22 | useEffect(() => { 23 | const eventListener = (event: MessageEvent>) => { 24 | const { action: eventAction, data } = event.data; 25 | 26 | if (savedHandler.current) { 27 | if (eventAction === action) { 28 | savedHandler.current(data); 29 | } 30 | } 31 | }; 32 | 33 | window.addEventListener('message', eventListener); 34 | // Remove Event Listener on component cleanup 35 | return () => window.removeEventListener('message', eventListener); 36 | }, [action]); 37 | }; 38 | -------------------------------------------------------------------------------- /server/modules/callbacks.lua: -------------------------------------------------------------------------------- 1 | lib.callback.register('pure-crafting:getItems', function(source, benchId) 2 | local bench = ActiveBenches[tostring(benchId)] 3 | return generateItems(source, bench.benchId, bench.type) 4 | end) 5 | 6 | lib.callback.register('pure-crafting:getData', function(source, benchId) 7 | local bench = ActiveBenches[tostring(benchId)] 8 | if not bench then return end 9 | 10 | local bps = getBlueprintsFromType(bench.type) 11 | for i = #bps, 1, -1 do 12 | if bench.bpHash[bps[i].blueprintId] then 13 | table.remove(bps, i) 14 | end 15 | end 16 | 17 | local data = { 18 | queue = bench.items, 19 | finished = bench.finished, 20 | blueprints = bps, 21 | } 22 | return data 23 | end) 24 | 25 | lib.callback.register('pure-crafting:createBench', function(source, coords, rotation, type) 26 | return insertBench(coords, rotation, source, type) 27 | end) 28 | 29 | lib.callback.register('pure-crafting:serverChecks', function(source) 30 | return serverChecks(source) 31 | end) 32 | 33 | lib.callback.register('pure-crafting:pickupBench', function(source, benchId) 34 | return pickupBench(source, benchId) 35 | end) -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "homepage": "web/build", 4 | "private": true, 5 | "version": "0.1.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "start:game": "vite build --watch", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-free": "^6.5.1", 15 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 16 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 17 | "@fortawesome/react-fontawesome": "^0.2.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@reduxjs/toolkit": "^1.9.5", 23 | "@types/react": "^18.0.25", 24 | "@types/react-dom": "^18.0.9", 25 | "@typescript-eslint/eslint-plugin": "^5.60.1", 26 | "@typescript-eslint/parser": "^5.60.1", 27 | "@vitejs/plugin-react": "^2.2.0", 28 | "eslint": "^8.43.0", 29 | "eslint-plugin-react-hooks": "^4.6.0", 30 | "eslint-plugin-react-refresh": "^0.4.5", 31 | "react-redux": "^8.1.1", 32 | "sass": "^1.63.6", 33 | "typescript": "^4.9.3", 34 | "vite": "^3.2.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/components/Crafting/Blueprint/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 7vw; 3 | height: 12.7vh; 4 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.35)); 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | flex-direction: column; 9 | cursor: pointer; 10 | overflow: hidden; 11 | } 12 | 13 | .fave { 14 | position: absolute; 15 | font-size: 0.75vw; 16 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.45)); 17 | margin-top: -10vh; 18 | margin-left: 5.3vw; 19 | } 20 | 21 | .icon { 22 | padding-bottom: 1vh; 23 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.35)); 24 | width: 7vh; 25 | height: 7vh; 26 | opacity: 0.4; 27 | -webkit-filter: grayscale(50%); 28 | -moz-filter: grayscale(50%); 29 | -o-filter: grayscale(50%); 30 | -ms-filter: grayscale(50%); 31 | filter: grayscale(50%); 32 | } 33 | 34 | .text { 35 | font-size: 0.9vw; 36 | position: absolute; 37 | margin-top: 9vh; 38 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 39 | 0px 4px 7px rgba(0, 0, 0, 0.25); 40 | width: 8.5vw; 41 | padding: 0 0.8vw; 42 | overflow: hidden; 43 | text-align: center; 44 | white-space: nowrap; 45 | text-overflow: ellipsis; 46 | } 47 | -------------------------------------------------------------------------------- /web/src/components/Crafting/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 69vh; 5 | width: 37.5vw; 6 | gap: 1.5vh; 7 | } 8 | 9 | .top { 10 | width: 100%; 11 | height: 5.5vh; 12 | display: flex; 13 | flex-direction: row; 14 | gap: 0.5vw; 15 | margin-left: 0.05vw; 16 | } 17 | 18 | .housing { 19 | width: 100%; 20 | height: 75vh; 21 | overflow: auto; 22 | display: grid; 23 | grid-template-columns: repeat(5, 1fr); 24 | grid-template-rows: repeat(5, 1fr); 25 | gap: 0.75vw 0.3vw; 26 | } 27 | 28 | .categories { 29 | width: 5.8vw; 30 | height: 100%; 31 | display: flex; 32 | flex-direction: row; 33 | gap: 0.5vw; 34 | overflow-x: hidden; 35 | scrollbar-width: none; 36 | } 37 | 38 | .search { 39 | width: 27.8vw; 40 | height: 100%; 41 | display: flex; 42 | justify-content: flex-start; 43 | align-items: center; 44 | gap: 0.8vw; 45 | padding: 0 0.8vw; 46 | box-shadow: rgb(0, 0, 0, 0.35) 0px 0px 16px; 47 | } 48 | 49 | .input { 50 | width: 21vw; 51 | height: 80%; 52 | border: none; 53 | outline: none; 54 | font-size: 1vw; 55 | background: transparent; 56 | margin-top: -0.3vh; 57 | } 58 | 59 | .input::placeholder { 60 | color: #fff; 61 | } 62 | -------------------------------------------------------------------------------- /web/build/assets/index.c1bfc35e.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap";._container_1t7q6_5{position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;width:100%;z-index:9999}._popup_1t7q6_27{padding:0 1vw;height:10.7vh;width:23vh;border-radius:.15vw;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:.7vw;box-shadow:#0000004d 0 0 16px;background:radial-gradient(60% 60% at 50% 50%,rgba(67,67,67,.9) 0%,rgba(37,37,37,.9) 100%)}._text_1t7q6_63{text-shadow:3px 0px 7px rgba(0,0,0,.05),-3px 0px 7px rgba(0,0,0,.05),0px 4px 7px rgba(0,0,0,.05);font-size:1.3vh;text-align:center;font-weight:bolder;font-family:Inter}._text2_1t7q6_81{text-shadow:3px 0px 7px rgba(0,0,0,.075),-3px 0px 7px rgba(0,0,0,.075),0px 4px 7px rgba(0,0,0,.075);font-size:1.1vh;text-align:center;font-weight:650;font-family:Inter}._boxes_1t7q6_99{display:flex;align-items:center;justify-content:center;gap:.7vw}._button_1t7q6_113{width:9.2vh;height:2.7vh;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:#0003 0 0 8px;font-size:1.15vh;text-shadow:3px 0px 7px rgba(0,0,0,.25),-3px 0px 7px rgba(0,0,0,.25),0px 4px 7px rgba(0,0,0,.25);border-radius:.2vw;font-family:Inter;font-weight:600;transition:.3s;background-color:#158d13} 2 | -------------------------------------------------------------------------------- /web/src/store/stores/popup/popup.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface popupState { 4 | showPopup: boolean; 5 | popupTitle: string; 6 | popupText: string; 7 | onSubmit: () => void; 8 | onCancel: () => void; 9 | } 10 | 11 | const initialState: popupState = { 12 | showPopup: false, 13 | popupTitle: '', 14 | popupText: '', 15 | onSubmit: () => { 16 | console.log('onSubmit'); 17 | }, 18 | onCancel: () => { 19 | console.log('onCancel'); 20 | }, 21 | }; 22 | 23 | export const popupSlice = createSlice({ 24 | name: 'popup', 25 | initialState, 26 | reducers: { 27 | setPopup: (state, action: PayloadAction) => { 28 | state.showPopup = action.payload.showPopup; 29 | state.popupTitle = action.payload.popupTitle; 30 | state.popupText = action.payload.popupText; 31 | state.onSubmit = action.payload.onSubmit; 32 | state.onCancel = action.payload.onCancel; 33 | }, 34 | hidePopup: (state) => { 35 | state.showPopup = false; 36 | }, 37 | changeText: (state, action: PayloadAction) => { 38 | state.popupText = action.payload; 39 | }, 40 | }, 41 | }); 42 | 43 | export default popupSlice.reducer; 44 | export const { setPopup, hidePopup, changeText } = popupSlice.actions; 45 | -------------------------------------------------------------------------------- /web/src/components/Info/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '../../../store/store'; 2 | import style from './index.module.css'; 3 | 4 | interface Props { 5 | amount: number; 6 | myAmount: number; 7 | name: string; 8 | image: string; 9 | } 10 | 11 | const Item = (props: Props) => { 12 | const theme = useAppSelector((state) => state.config.theme); 13 | const number = useAppSelector((state) => state.crafting.amount); 14 | const hasEnough = props.myAmount - props.amount * number >= 0; 15 | return ( 16 |
17 |
23 | 24 |

29 | {props.myAmount}/{props.amount * number} 30 |

31 |
32 |

37 | {props.name} 38 |

39 |
40 | ); 41 | }; 42 | 43 | export default Item; 44 | -------------------------------------------------------------------------------- /client/events.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent('pure-crafting:addToQueue', function(item) 2 | SendReactMessage('addToQueue', item) 3 | end) 4 | 5 | RegisterNetEvent('pure-crafting:finishedCraft', function(items, finished) 6 | SendReactMessage('updateItems', items) 7 | SendReactMessage('updateFinished', finished) 8 | end) 9 | 10 | RegisterNetEvent('pure-crafting:updateFinished', function(data) 11 | SendReactMessage('updateFinished', data) 12 | end) 13 | 14 | RegisterNetEvent('pure-crafting:secondChange', function(itemsSeconds) 15 | SendReactMessage('secondsChange', itemsSeconds) 16 | end) 17 | 18 | RegisterNetEvent('pure-crafting:updateQueue', function(data) 19 | SendReactMessage('updateItems', data) 20 | end) 21 | 22 | RegisterNetEvent('pure-crafting:updateItems', function(items) 23 | SendReactMessage('itemsChange', items) 24 | end) 25 | 26 | RegisterNetEvent('pure-crafting:generateItems', function(benId) 27 | if not currentZone then 28 | return 29 | end 30 | local benchId = currentZone.benchId 31 | if benId == benchId then 32 | local items = lib.callback.await('pure-crafting:getItems', false, benchId) 33 | SendReactMessage('itemsChange', items) 34 | end 35 | end) 36 | 37 | RegisterNetEvent('pure-crafting:setFaves', function(faves) 38 | SendReactMessage('setFaves', faves) 39 | end) -------------------------------------------------------------------------------- /web/src/components/Info/Bottom/index.module.css: -------------------------------------------------------------------------------- 1 | .buttonHousing { 2 | width: 7.7vw; 3 | height: 4.2vh; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .button { 10 | height: 4.2vh; 11 | width: 4vw; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | font-size: 1.6vw; 16 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.3), -3px 0px 7px rgba(0, 0, 0, 0.3), 17 | 0px 4px 7px rgba(0, 0, 0, 0.3); 18 | cursor: pointer; 19 | position: relative; 20 | overflow: hidden; 21 | } 22 | 23 | .buttonText { 24 | margin-top: -0.35vh; 25 | } 26 | 27 | .craftButton { 28 | height: 4.2vh; 29 | width: 9.7vw; 30 | padding: 0 0.3vw; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | font-size: 1.18vw; 35 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.3), -3px 0px 7px rgba(0, 0, 0, 0.3), 36 | 0px 4px 7px rgba(0, 0, 0, 0.3); 37 | cursor: pointer; 38 | position: relative; 39 | overflow: hidden; 40 | } 41 | 42 | .input { 43 | outline: 0; 44 | border: 0; 45 | width: 3vw; 46 | height: 4.2vh; 47 | text-align: center; 48 | font-size: 1.18vw; 49 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 50 | 0px 4px 7px rgba(0, 0, 0, 0.25); 51 | } 52 | 53 | .input::placeholder { 54 | color: #fff; 55 | } 56 | -------------------------------------------------------------------------------- /web/src/components/Crafting/Blueprint/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDistpatch, useAppSelector } from '../../../store/store'; 2 | import style from './index.module.css'; 3 | import { setSelected } from '../../../store/stores/crafting/crafting'; 4 | import { useEffect } from 'react'; 5 | import updateRipples from '../../../utils/updateRipples'; 6 | 7 | interface Props { 8 | name: string; 9 | image: string; 10 | isFave: boolean; 11 | id: number; 12 | selected: boolean; 13 | type: string; 14 | } 15 | 16 | const Blueprint = (props: Props) => { 17 | const theme = useAppSelector((state) => state.config.theme); 18 | const dispatch = useAppDistpatch(); 19 | 20 | useEffect(() => { 21 | updateRipples(); 22 | }); 23 | return ( 24 |
{ 32 | dispatch( 33 | setSelected({ 34 | id: props.id, 35 | type: props.type, 36 | }) 37 | ); 38 | }}> 39 | 40 |

45 | {props.name} 46 |

47 |
48 | ); 49 | }; 50 | 51 | export default Blueprint; 52 | -------------------------------------------------------------------------------- /server/modules/faves.lua: -------------------------------------------------------------------------------- 1 | function User:addFave(itemName) 2 | self.faves[itemName] = true 3 | local affectedRows = MySQL.update.await('UPDATE crafting_users SET faves = ? WHERE uniqueId = ?', { 4 | json.encode(self.faves), self.uniqueId 5 | }) 6 | notifySystem(self.source, { 7 | title = _Lang('faves.add'), 8 | type = 'success', 9 | position = Config.libText.notfiyPoistion, 10 | }) 11 | TriggerClientEvent('pure-crafting:setFaves', self.source, self.faves) 12 | end 13 | 14 | function User:removeFave(itemName) 15 | self.faves[itemName] = nil 16 | local affectedRows = MySQL.update.await('UPDATE crafting_users SET faves = ? WHERE uniqueId = ?', { 17 | json.encode(self.faves), self.uniqueId 18 | }) 19 | notifySystem(self.source, { 20 | title = _Lang('faves.remove'), 21 | type = 'success', 22 | position = Config.libText.notfiyPoistion, 23 | }) 24 | TriggerClientEvent('pure-crafting:setFaves', self.source, self.faves) 25 | end 26 | 27 | function User:checkFave(itemName) 28 | if self.faves[itemName] then 29 | return true 30 | end 31 | return 32 | end 33 | 34 | function createUserSetFave(source, itemName) 35 | local uniqueId = getPlayerUniqueId(source) 36 | MySQL.insert.await('INSERT INTO `crafting_users` (uniqueId) VALUES (?)', { 37 | uniqueId 38 | }) 39 | local user = User:new(source) 40 | Players[tostring(source)] = user 41 | Wait(150) 42 | user:addFave(itemName) 43 | end -------------------------------------------------------------------------------- /locales/translations/en.lua: -------------------------------------------------------------------------------- 1 | Language['en'] = { 2 | nui = { 3 | craft = 'Craft', 4 | language = 'Items Required:', 5 | craftTime = 'Crafting Time:', 6 | s = 's', 7 | uses = 'Uses:', 8 | claim = 'CONFIRM', 9 | cancel = 'CANCEL', 10 | yes = 'Yes', 11 | no = 'No', 12 | claimCraft = 'Claim Craft', 13 | cancelCraft = 'Cancel Craft', 14 | areYouSure = 'Are you sure you want to do this?', 15 | noItemSelected = 'No item selected', 16 | required = 'Items Required:', 17 | unlockBP = 'Unlock Blueprint', 18 | }, 19 | useBench = 'Press [E] to use the bench.', 20 | blueprints = { 21 | notAtABench = 'You are not at a bench.', 22 | alreadyUsed = 'Blueprint already used.', 23 | added = 'Blueprint added.', 24 | cantUse = 'You can not use this blueprint on this bench.', 25 | }, 26 | placingBench = { 27 | left = 'Press Q to rotate left', 28 | right = 'Press E to rotate right', 29 | place = 'Press F to place', 30 | cancel = 'Press X to cancel', 31 | inAnotherZone = 'You are too close to another bench.', 32 | limitReached = 'You have reached the limit of benches you can place.', 33 | }, 34 | notEnough = { 35 | title = 'Not Enough Items', 36 | message = 'You do not have enough items to craft this.', 37 | }, 38 | faves = { 39 | add = 'Added to Favourites', 40 | remove = 'Removed from Favourites', 41 | }, 42 | 43 | } -------------------------------------------------------------------------------- /web/src/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Kanit:wght@600&display=swap'); 2 | 3 | @font-face { 4 | font-family: 'Bold'; 5 | font-weight: 700; 6 | src: local('Segoe UI Bold'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Black'; 11 | src: url('./assests/seguibl.ttf'); 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | width: 7px; 16 | } 17 | 18 | /* Track */ 19 | ::-webkit-scrollbar-track { 20 | background: #212121; 21 | } 22 | 23 | /* TODO: last ting make this interchangeable like inclothing */ 24 | ::-webkit-scrollbar-thumb { 25 | background: #333333; 26 | } 27 | 28 | ::-webkit-scrollbar-thumb:horizontal { 29 | min-width: 2px !important; /* Minimum width for the thumb */ 30 | } 31 | 32 | * { 33 | box-sizing: border-box; 34 | margin: 0; 35 | padding: 0; 36 | outline: 0; 37 | border: 0; 38 | user-select: none; 39 | font-family: 'Bold'; 40 | } 41 | 42 | input::-webkit-outer-spin-button, 43 | input::-webkit-inner-spin-button { 44 | -webkit-appearance: none; 45 | margin: 0; 46 | } 47 | 48 | .ripple-animation { 49 | overflow: hidden; 50 | position: relative; 51 | } 52 | 53 | .ripple-effect { 54 | position: absolute; 55 | background: #f7f7f7; 56 | transform: translate(-50%, -50%); 57 | pointer-events: none; 58 | border-radius: 50%; 59 | animation: animate 0.6s linear infinite; 60 | } 61 | 62 | @keyframes animate { 63 | 0% { 64 | width: 0; 65 | height: 0; 66 | opacity: 0.35; 67 | } 68 | 100% { 69 | width: 100px; 70 | height: 100px; 71 | opacity: 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /web/build/assets/index.f5b612d0.js: -------------------------------------------------------------------------------- 1 | import{u as n,j as o,a as s}from"./index.4aefa6c8.js";const l="_container_1t7q6_5",i="_popup_1t7q6_27",u="_text_1t7q6_63",d="_text2_1t7q6_81",p="_boxes_1t7q6_99",g="_button_1t7q6_113",e={container:l,popup:i,text:u,text2:d,boxes:p,button:g},_=()=>{const a=n(t=>t.popup),r=n(t=>t.config.theme),c=n(t=>t.config.language);return o("div",{className:e.container,onClick:t=>{!t.target||typeof t.target.className!="string"||t.target.className.includes("container")&&a.onCancel()},children:s("div",{className:e.popup,style:{color:r.white},children:[o("h1",{className:e.text,children:a.popupTitle}),o("h1",{className:e.text2,children:a.popupText}),s("div",{className:e.boxes,style:{color:r.white},children:[o("div",{className:e.button,onClick:()=>{a.onSubmit()},onMouseOver:t=>{t.currentTarget.style.background="radial-gradient(50% 50% at 50% 50%, #4ADF47 0%, #158d13 100%)"},onMouseOut:t=>{t.currentTarget.style.background="radial-gradient(60% 60% at 50% 50%, #4ADF47 0%, #169814 100%)"},style:{background:"radial-gradient(60% 60% at 50% 50%, #4ADF47 0%, #169814 100%)",border:"0.15vw solid rgba(70, 255, 78, 1)",color:"rgba(0, 253, 25, 1)"},children:c.claim}),o("div",{className:e.button,onClick:()=>{a.onCancel()},onMouseOver:t=>{t.currentTarget.style.background="radial-gradient(50% 50% at 50% 50%, #DB3F3F 0%, #a51919 100%)"},onMouseOut:t=>{t.currentTarget.style.background="radial-gradient(60% 60% at 50% 50%, #DB3F3F 0%, #AA2020 100%)"},style:{background:"radial-gradient(60% 60% at 50% 50%, #DB3F3F 0%, #AA2020 100%)",border:"0.15vw solid rgba(234, 47, 47, 1)",color:"rgba(255, 61, 61, 1)"},children:c.cancel})]})]})})};export{_ as default}; 2 | -------------------------------------------------------------------------------- /server/modules/items.lua: -------------------------------------------------------------------------------- 1 | function generateItems(source, benchId, type) 2 | local items = {} 3 | local itemsToSearch = Config.items[type] 4 | if not itemsToSearch then return items end 5 | for i = 1, #itemsToSearch do 6 | local item = itemsToSearch[i] 7 | local requiredItems = item.requiredItems 8 | for i = 1, #requiredItems do 9 | local requiredItem = requiredItems[i] 10 | local myAmount = 0 11 | local myItem = getItems(source, requiredItem.itemName) 12 | if not myItem then 13 | myAmount = 0 14 | else 15 | myAmount = myItem.amount 16 | end 17 | requiredItem.myAmount = myAmount 18 | end 19 | items[i] = item 20 | end 21 | local bench = ActiveBenches[tostring(benchId)] 22 | if not bench then 23 | return items 24 | end 25 | local blueprints = bench.blueprints 26 | for i = 1, #blueprints do 27 | local blueprint = blueprints[i] 28 | local blueprintTable = getBlueprintFromId(blueprint, type) 29 | if not blueprintTable then goto continue end 30 | local blueprintRequiredItems = blueprintTable.requiredItems 31 | for i = 1, #blueprintRequiredItems do 32 | local requiredItem = blueprintRequiredItems[i] 33 | local myAmount = 0 34 | local myItem = getItems(source, requiredItem.itemName) 35 | if not myItem then 36 | myAmount = 0 37 | else 38 | myAmount = myItem.amount 39 | end 40 | requiredItem.myAmount = myAmount 41 | end 42 | items[#items + 1] = blueprintTable 43 | ::continue:: 44 | end 45 | return items 46 | end -------------------------------------------------------------------------------- /web/src/store/stores/crafting/queue.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { item, queue } from '../../../types/queue'; 3 | // import { sendNui } from '../../../utils/sendNui'; 4 | 5 | const initialState: queue = { 6 | finished: [ 7 | { 8 | itemName: 'wood', 9 | image: 'https://i.imgur.com/MMsrX15.jpeg', 10 | secondsLeft: 20, 11 | timeStarted: 1709554540, 12 | timeToCraft: 20, 13 | id: 0, 14 | }, 15 | ], 16 | items: [ 17 | { 18 | itemName: 'wood', 19 | image: 'https://i.imgur.com/MMsrX15.jpeg', 20 | secondsLeft: 40, 21 | timeStarted: 1709554967, 22 | timeToCraft: 20, 23 | id: 0, 24 | }, 25 | { 26 | itemName: 'wood', 27 | image: 'https://i.imgur.com/MMsrX15.jpeg', 28 | secondsLeft: 800, 29 | timeToCraft: 20, 30 | timeStarted: 1709554757, 31 | id: 0, 32 | }, 33 | ], 34 | }; 35 | 36 | export const queueSlice = createSlice({ 37 | name: 'queue', 38 | initialState, 39 | reducers: { 40 | addItem: (state, action: PayloadAction) => { 41 | state.items.push(action.payload); 42 | // sendNui('queueCraft', { 43 | // item: action.payload, 44 | // }); 45 | }, 46 | setQueueItems: (state, action: PayloadAction) => { 47 | state.items = action.payload; 48 | }, 49 | setQueueFinishedItems: (state, action: PayloadAction) => { 50 | state.finished = action.payload; 51 | }, 52 | updateSecondsChange: (state, action: PayloadAction) => { 53 | state.items[0].secondsLeft = action.payload; 54 | }, 55 | }, 56 | }); 57 | 58 | export default queueSlice.reducer; 59 | export const { 60 | addItem, 61 | setQueueItems, 62 | setQueueFinishedItems, 63 | updateSecondsChange, 64 | } = queueSlice.actions; 65 | -------------------------------------------------------------------------------- /web/src/components/Crafting/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { useAppDistpatch, useAppSelector } from '../../../store/store'; 3 | import style from './index.module.css'; 4 | import { faStar } from '@fortawesome/free-solid-svg-icons'; 5 | import { setSelected } from '../../../store/stores/crafting/crafting'; 6 | import { useEffect } from 'react'; 7 | import updateRipples from '../../../utils/updateRipples'; 8 | 9 | interface Props { 10 | name: string; 11 | image: string; 12 | isFave: boolean; 13 | id: number; 14 | selected: boolean; 15 | type: string; 16 | } 17 | 18 | const Item = (props: Props) => { 19 | const theme = useAppSelector((state) => state.config.theme); 20 | const config = useAppSelector((state) => state.config.config); 21 | const dispatch = useAppDistpatch(); 22 | 23 | useEffect(() => { 24 | updateRipples(); 25 | }); 26 | return ( 27 |
{ 35 | dispatch( 36 | setSelected({ 37 | id: props.id, 38 | type: props.type, 39 | }) 40 | ); 41 | }}> 42 | {props.isFave && config.enableFavourites && ( 43 | 50 | )} 51 | 52 |

57 | {props.name} 58 |

59 |
60 | ); 61 | }; 62 | 63 | export default Item; 64 | -------------------------------------------------------------------------------- /web/src/components/Popup/index.module.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); 2 | 3 | .container { 4 | position: absolute; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | height: 100%; 10 | width: 100%; 11 | z-index: 9999; 12 | } 13 | 14 | .popup { 15 | padding: 0 1vw; 16 | height: 10.7vh; 17 | width: 23vh; 18 | border-radius: 0.15vw; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | flex-direction: column; 23 | gap: 0.7vw; 24 | box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 16px; 25 | background: radial-gradient( 26 | 60% 60% at 50% 50%, 27 | rgba(67, 67, 67, 0.9) 0%, 28 | rgba(37, 37, 37, 0.9) 100% 29 | ); 30 | } 31 | 32 | .text { 33 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.05), -3px 0px 7px rgba(0, 0, 0, 0.05), 34 | 0px 4px 7px rgba(0, 0, 0, 0.05); 35 | font-size: 1.3vh; 36 | text-align: center; 37 | font-weight: bolder; 38 | font-family: 'Inter'; 39 | } 40 | 41 | .text2 { 42 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.075), 43 | -3px 0px 7px rgba(0, 0, 0, 0.075), 0px 4px 7px rgba(0, 0, 0, 0.075); 44 | font-size: 1.1vh; 45 | text-align: center; 46 | font-weight: 650; 47 | font-family: 'Inter'; 48 | } 49 | 50 | .boxes { 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | gap: 0.7vw; 55 | } 56 | 57 | .button { 58 | width: 9.2vh; 59 | height: 2.7vh; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | cursor: pointer; 64 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 8px; 65 | font-size: 1.15vh; 66 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 67 | 0px 4px 7px rgba(0, 0, 0, 0.25); 68 | border-radius: 0.2vw; 69 | font-family: 'Inter'; 70 | font-weight: 600; 71 | transition: 0.3s; 72 | background-color: #158d13; 73 | } 74 | -------------------------------------------------------------------------------- /web/src/providers/VisibilityProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Context, 3 | createContext, 4 | useContext, 5 | useEffect, 6 | useState, 7 | } from 'react'; 8 | import { useNuiEvent } from '../hooks/useNuiEvent'; 9 | import { fetchNui } from '../utils/fetchNui'; 10 | import { isEnvBrowser } from '../utils/misc'; 11 | import updateRipples from '../utils/updateRipples'; 12 | 13 | const VisibilityCtx = createContext(null); 14 | 15 | interface VisibilityProviderValue { 16 | setVisible: (visible: boolean) => void; 17 | visible: boolean; 18 | } 19 | 20 | // This should be mounted at the top level of your application, it is currently set to 21 | // apply a CSS visibility value. If this is non-performant, this should be customized. 22 | export const VisibilityProvider: React.FC<{ children: React.ReactNode }> = ({ 23 | children, 24 | }) => { 25 | const [visible, setVisible] = useState(false); 26 | 27 | useNuiEvent('setVisible', setVisible); 28 | 29 | // Handle pressing escape/backspace 30 | useEffect(() => { 31 | if (!visible) return; 32 | 33 | const keyHandler = (e: KeyboardEvent) => { 34 | if (['Escape'].includes(e.code)) { 35 | if (!isEnvBrowser()) fetchNui('hideFrame'); 36 | else setVisible(!visible); 37 | } 38 | }; 39 | 40 | window.addEventListener('keydown', keyHandler); 41 | updateRipples(); 42 | return () => window.removeEventListener('keydown', keyHandler); 43 | }, [visible]); 44 | 45 | return ( 46 | 51 |
52 | {children} 53 |
54 |
55 | ); 56 | }; 57 | 58 | export const useVisibility = () => 59 | useContext( 60 | VisibilityCtx as Context 61 | ); 62 | -------------------------------------------------------------------------------- /server/inventory/esxInventory.lua: -------------------------------------------------------------------------------- 1 | if Config.inventory ~= 'esx' then return end 2 | 3 | function removeItem(source, item, amount) 4 | local player = ESX.GetPlayerFromId(source) 5 | return player.removeInventoryItem(item, amount) 6 | end 7 | 8 | function checkItem(source, item, amount) 9 | local player = ESX.GetPlayerFromId(source) 10 | local esxAmount = player.getInventoryItem(item).count 11 | if esxAmount >= amount then return true end 12 | return false 13 | end 14 | 15 | function getItems(source, item) 16 | local player = ESX.GetPlayerFromId(source) 17 | local amt = { 18 | amount = 0, 19 | } 20 | if not player.getInventoryItem(item) then return amt end 21 | local plyerAmt = player.getInventoryItem(item).count 22 | amt.amount = plyerAmt 23 | return amt 24 | end 25 | 26 | function giveItem(source, item, amount) 27 | local player = ESX.GetPlayerFromId(source) 28 | return player.addInventoryItem(item, amount) 29 | end 30 | 31 | function createItem(name, trigger, data) 32 | ESX.RegisterUsableItem(name, function(source) 33 | TriggerClientEvent(trigger, source, data) 34 | end) 35 | end 36 | 37 | -- THESE ARE EXAMPLES OF BLUEPRINTS / FOLLOW THE DOCS FOR MORE INFORMATION 38 | 39 | --[[ 40 | INSERT INTO `items` (name, label, weight) VALUES 41 | ('weap_bench', 'Weapons Bench', 2), 42 | ('misc_bench', 'Misc Bench', 2), 43 | ('blueprint_molotov', 'Molotov Blueprint', 2), 44 | ('blueprint_grip', 'Grip Blueprint', 2), 45 | ('blueprint_suppressor', 'Suppressor Blueprint', 2), 46 | ('blueprint_extendedclip', 'Extended Clip Blueprint', 2), 47 | ('blueprint_scope', 'Scope Blueprint', 2), 48 | ('blueprint_specialcarbine', 'Special Carbine Blueprint', 2), 49 | ('blueprint_assaultrifle', 'Assaultrifle Blueprint', 2), 50 | ('blueprint_advancedrifle', 'Advanced Rifle Blueprint', 2), 51 | ('blueprint_sawnoffshotgun', 'Sawn Off Shotgun Blueprint', 2), 52 | ('blueprint_machinepistol', 'Machine Pistol Blueprint', 2), 53 | ('blueprint_microsmg', 'Micro SMG Blueprint', 2), 54 | ]]-- -------------------------------------------------------------------------------- /web/src/components/Info/Required/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useAppSelector } from '../../../store/store'; 3 | import Item from '../Item'; 4 | import style from './index.module.css'; 5 | 6 | interface Props { 7 | type: string; 8 | } 9 | 10 | const Required = (props: Props) => { 11 | const theme = useAppSelector((state) => state.config.theme); 12 | const language = useAppSelector((state) => state.config.language); 13 | const item = useAppSelector((state) => state.crafting.currentItem); 14 | const scrollRef = useRef(null); 15 | 16 | const handleWheel = (evt: any) => { 17 | const delta = evt.deltaY; 18 | if (scrollRef.current) { 19 | scrollRef.current.scrollLeft += delta; 20 | } 21 | }; 22 | 23 | if (props.type === 'blaze') { 24 | return ( 25 |
31 |

36 | {language.unlockBP} 37 |

38 |
39 | ); 40 | } 41 | 42 | return ( 43 |
49 |
50 | {item && 51 | item.requiredItems.length > 0 && 52 | item.requiredItems.map((req, index) => { 53 | return ( 54 | 61 | ); 62 | })} 63 | {/* 64 | 65 | 66 | 67 | 68 | */} 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default Required; 75 | -------------------------------------------------------------------------------- /client/nuiCallbacks.lua: -------------------------------------------------------------------------------- 1 | RegisterNUICallback('getConfig', function(_, cb) 2 | debugPrint('getConfig') 3 | if not Config.enableFavourites then 4 | Config.categories[1] = nil 5 | end 6 | cb(Config) 7 | end) 8 | 9 | RegisterNUICallback('getLanguage', function(_, cb) 10 | local lang = Config.language 11 | if not Language[lang] then 12 | lang = 'en' 13 | end 14 | debugPrint('getLanguage', lang) 15 | cb(Language[lang].nui) 16 | end) 17 | 18 | RegisterNUICallback('hideFrame', function(_, cb) 19 | toggleNuiFrame(false) 20 | TriggerScreenblurFadeOut(250) 21 | debugPrint('Hide NUI frame') 22 | cb({}) 23 | end) 24 | 25 | RegisterNUICallback('attemptCraft', function(data, cb) 26 | debugPrint('RegisterNUICallback | attemptCraft', json.encode(data)) 27 | if not currentZone then return end 28 | local benchId = currentZone.benchId 29 | TriggerServerEvent('pure-crafting:attemptCraft', benchId, data) 30 | end) 31 | 32 | RegisterNUICallback('craftFinished', function(data, cb) 33 | debugPrint('RegisterNUICallback | craftFinished', json.encode(data)) 34 | if not currentZone then return end 35 | local benchId = currentZone.benchId 36 | TriggerServerEvent('pure-crafting:craftFinished', benchId, data) 37 | end) 38 | 39 | RegisterNUICallback('claimCraft', function(data, cb) 40 | debugPrint('RegisterNUICallback | claimCraft', json.encode(data)) 41 | if not currentZone then return end 42 | local benchId = currentZone.benchId 43 | TriggerServerEvent('pure-crafting:claimCraft', benchId, data) 44 | end) 45 | 46 | RegisterNUICallback('cancelCraft', function(data, cb) 47 | debugPrint('RegisterNUICallback | cancelCraft', json.encode(data)) 48 | if not currentZone then return end 49 | local benchId = currentZone.benchId 50 | TriggerServerEvent('pure-crafting:cancelCraft', benchId, data) 51 | end) 52 | 53 | RegisterNUICallback('notEnoughItems', function() 54 | debugPrint('RegisterNUICallback | notEnoughItems') 55 | notifySystem({ 56 | title = _Lang('notEnough.title'), 57 | type = 'error', 58 | position = Config.libText.notfiyPoistion, 59 | }) 60 | end) 61 | 62 | RegisterNUICallback('setFavourite', function(data) 63 | TriggerServerEvent('pure-crafting:setFavourite', data) 64 | end) -------------------------------------------------------------------------------- /server/craft/claimCraft.lua: -------------------------------------------------------------------------------- 1 | function Queue:claimCraft(id, index, source) 2 | local index = index + 1 3 | local item = self.finished[index] 4 | if not item then 5 | debugPrint('Queue:claimCraft | failed - no item', index, json.encode(self.finished)) 6 | return 7 | end 8 | local itemData = getItemFromId(tostring(id), self.type) 9 | if not itemData then 10 | debugPrint('Queue:claimCraft | failed - no itemData', id, json.encode(CraftableItems)) 11 | return 12 | end 13 | giveItem(source, itemData.itemName, 1) 14 | table.remove(self.finished, index) 15 | self:triggerEvent('pure-crafting:updateFinished', self.finished) 16 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET finished = ? WHERE id = ?', { 17 | json.encode(self.finished), self.benchId 18 | }) 19 | debugPrint('Queue:claimCraft | ', json.encode(item), json.encode(self.finished)) 20 | local items = generateItems(source, self.benchId, self.type) 21 | TriggerClientEvent('pure-crafting:updateItems', source, items) 22 | return true 23 | end 24 | 25 | function Queue:cancelCraft(id, index, source) 26 | local index = index + 1 27 | local item = self.items[index] 28 | if not item then 29 | debugPrint('Queue:cancelCraft | failed - no item', index, json.encode(self.items)) 30 | return 31 | end 32 | local itemData = getItemFromId(tostring(id), self.type) 33 | if not itemData then 34 | debugPrint('Queue:cancelCraft | failed - no itemData', id, json.encode(Config.items[self.type])) 35 | return 36 | end 37 | for i = 1, #itemData.requiredItems do 38 | local requiredItem = itemData.requiredItems[i] 39 | giveItem(source, requiredItem.itemName, requiredItem.amount) 40 | end 41 | table.remove(self.items, index) 42 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET queue = ? WHERE id = ?', { 43 | json.encode(self.items), self.benchId 44 | }) 45 | self:triggerEvent('pure-crafting:updateQueue', self.items) 46 | local items = generateItems(source, self.benchId, self.type) 47 | TriggerClientEvent('pure-crafting:updateItems', source, items) 48 | debugPrint('Queue:cancelCraft | ', json.encode(item), json.encode(self.items)) 49 | if itemData.blueprintId and not Config.unlimitedBlueprints then 50 | self:useBlueprint(source, itemData.blueprintId) 51 | end 52 | return true 53 | end -------------------------------------------------------------------------------- /server/queue.lua: -------------------------------------------------------------------------------- 1 | Queue = {} 2 | ActiveBenches = {} 3 | User = {} 4 | Players = {} 5 | 6 | 7 | function Queue:new(benchId, queue, finished, blueprints, type, userPlaced) 8 | local decodeBps = json.decode(blueprints) 9 | local bpHash = {} 10 | 11 | if decodeBps then 12 | for i = 1, #decodeBps do 13 | bpHash[decodeBps[i]] = true 14 | end 15 | end 16 | 17 | local data = { 18 | benchId = benchId, 19 | activeMembers = {}, 20 | items = json.decode(queue), 21 | finished = json.decode(finished), 22 | blueprints = decodeBps, 23 | bpHash = bpHash, 24 | userPlaced = userPlaced, 25 | type = type 26 | } 27 | 28 | debugPrint('Queue:new | ', json.encode(data)) 29 | return setmetatable(data, {__index = Queue}) 30 | end 31 | 32 | function initQueue(benchId, queue, finished, blueprints, type, userPlaced) 33 | local bench = Queue:new(benchId, queue, finished, blueprints, type, userPlaced) 34 | ActiveBenches[tostring(benchId)] = bench 35 | end 36 | 37 | function User:new(source) 38 | local uniqueId = getPlayerUniqueId(source) 39 | 40 | local row = MySQL.single.await('SELECT `faves`, `amountPlaced` FROM `crafting_users` WHERE `uniqueId` = ? LIMIT 1', { 41 | uniqueId 42 | }) 43 | 44 | if not row then 45 | debugPrint('User:new | No data for user: ', uniqueId) 46 | return 47 | end 48 | 49 | debugPrint('User:new | ', row.amountPlaced) 50 | 51 | local data = { 52 | source = source, 53 | uniqueId = uniqueId, 54 | faves = json.decode(row.faves), 55 | amountPlaced = row.amountPlaced, 56 | } 57 | 58 | debugPrint('User:new | ', json.encode(data)) 59 | 60 | return setmetatable(data, {__index = User}) 61 | end 62 | 63 | function initUser(source) 64 | local user = User:new(source) 65 | Players[tostring(source)] = user 66 | end 67 | 68 | function removeUser(source) 69 | Players[tostring(source)] = nil 70 | end 71 | 72 | function checkPerson(source) 73 | local user = Players[tostring(source)] 74 | if not user then 75 | local uniqueId = getPlayerUniqueId(source) 76 | 77 | local row = MySQL.single.await('SELECT `faves`, `amountPlaced` FROM `crafting_users` WHERE `uniqueId` = ? LIMIT 1', { 78 | uniqueId 79 | }) 80 | 81 | if not row then 82 | return 83 | end 84 | end 85 | return true 86 | end -------------------------------------------------------------------------------- /server/craft/craftItem.lua: -------------------------------------------------------------------------------- 1 | function Queue:craftItem(amount, item, source) 2 | local canCraftItem = canCraftItem(source, item, amount) 3 | if not canCraftItem then return end 4 | local items = generateItems(source, self.benchId, self.type) 5 | TriggerClientEvent('pure-crafting:updateItems', source, items) 6 | for i = 1, amount do 7 | newItem = { 8 | itemName = item.itemName, 9 | image = item.image, 10 | secondsLeft = item.craftingTime, 11 | timeStarted = os.time(), 12 | timeToCraft = item.craftingTime, 13 | id = item.id 14 | } 15 | self.items[#self.items + 1] = newItem 16 | self:triggerEvent('pure-crafting:addToQueue', newItem) 17 | end 18 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET queue = ? WHERE id = ?', { 19 | json.encode(self.items), self.benchId 20 | }) 21 | debugPrint('Queue:add | ', json.encode(item), json.encode(self.items)) 22 | self:triggerEvent('pure-crafting:updateQueue', self.items) 23 | if item.blueprintId and not Config.unlimitedBlueprints then 24 | self:removeBlueprint(source, item.blueprintId) 25 | end 26 | return true 27 | end 28 | 29 | function canCraftItem(source, item, amount) 30 | local requiredItems = item.requiredItems 31 | if not checkItems(source, requiredItems, amount) then 32 | notifySystem(source, { 33 | title = _Lang('notEnough.title'), 34 | description = _Lang('notEnough.message'), 35 | type = 'error', 36 | position = Config.libText.notfiyPoistion, 37 | }) 38 | return 39 | end 40 | if not removeItems(source, requiredItems, amount) then 41 | notifySystem(source, { 42 | title = _Lang('notEnough.title'), 43 | description = _Lang('notEnough.message'), 44 | type = 'error', 45 | position = Config.libText.notfiyPoistion, 46 | }) 47 | return 48 | end 49 | return true 50 | end 51 | 52 | function checkItems(source, items, amount) 53 | if not items or #items == 0 then return end 54 | for i = 1, #items do 55 | local item = items[i] 56 | if not checkItem(source, item.itemName, item.amount * amount) then 57 | return 58 | end 59 | end 60 | return true 61 | end 62 | 63 | function removeItems(source, items, amount) 64 | if not items or #items == 0 then return end 65 | for i = 1, #items do 66 | local item = items[i] 67 | if not removeItem(source, item.itemName, item.amount * amount) then 68 | return 69 | end 70 | end 71 | return true 72 | end -------------------------------------------------------------------------------- /client/zones/zones.lua: -------------------------------------------------------------------------------- 1 | currentZone = nil 2 | local zones = { 3 | benches = {}, 4 | } 5 | 6 | function getZoneFromId(id, zone) 7 | for i = 1, #zone do 8 | if zone[i].id == id then 9 | return i 10 | end 11 | end 12 | end 13 | 14 | function checkZone(coords) 15 | for i = 1, #zones.benches do 16 | local zone = zones.benches[i] 17 | if zone:contains(coords) then 18 | return true 19 | end 20 | end 21 | return 22 | end 23 | 24 | function removeZones() 25 | for i = 1, #zones.benches do 26 | zones.benches[i]:remove() 27 | end 28 | zones.benches = {} 29 | end 30 | 31 | function setupBenchZone(location, rotation, boxSize) 32 | zones.benches[#zones.benches + 1] = initiateZone({ 33 | coords = location, 34 | boxSize = boxSize, 35 | }, onBenchEnter, onZoneExit) 36 | end 37 | 38 | function initiateZone(table, onEnter, onExit) 39 | return lib.zones.box({ 40 | coords = vector3(table.coords.x, table.coords.y, table.coords.z), 41 | rotation = table.rotation, 42 | size = table.boxSize, 43 | debug = Config.debug, 44 | onEnter = onEnter, 45 | onExit = onExit, 46 | }) 47 | end 48 | 49 | function onBenchEnter(data) 50 | local text = _Lang('useBench') 51 | local benchId = getZoneFromId(data.id, zones.benches) 52 | local bench = Benches[benchId] 53 | if not bench then return end 54 | TriggerServerEvent('pure-crafting:enterZone', bench.id) 55 | currentZone = { 56 | name = 'benches', 57 | benchId = bench.id 58 | } 59 | if Config.targetingOptions.interaction == 'interaction' then 60 | lib.showTextUI(text) 61 | end 62 | end 63 | 64 | local sleep = 2000 65 | function loopZones() 66 | Wait(sleep) 67 | sleep = 1000 68 | while true do 69 | if currentZone then 70 | sleep = 1 71 | if IsControlJustReleased(0, 38) then 72 | if currentZone.name == 'benches' then 73 | TriggerEvent('pure-crafting:openCrafting') 74 | end 75 | end 76 | else 77 | sleep = 1000 78 | end 79 | Wait(sleep) 80 | end 81 | end 82 | 83 | function onZoneExit(data) 84 | lib.hideTextUI() 85 | local benchId = getZoneFromId(data.id, zones.benches) 86 | local bench = Benches[benchId] 87 | if not bench then return end 88 | TriggerServerEvent('pure-crafting:exitZone', bench.id) 89 | toggleNuiFrame(false) 90 | TriggerScreenblurFadeOut(250) 91 | currentZone = nil 92 | end 93 | 94 | CreateThread(function() 95 | Wait(100) 96 | if Config.targetingOptions.interaction == 'interaction' then 97 | loopZones() 98 | end 99 | end) 100 | 101 | AddEventHandler("onResourceStop", function(resource) 102 | if resource == GetCurrentResourceName() then 103 | removeZones() 104 | deleteAllBenches() 105 | end 106 | end) -------------------------------------------------------------------------------- /config/config.lua: -------------------------------------------------------------------------------- 1 | Config = { 2 | -- [[ Frameworks Supported ]] -- 3 | --[[ 4 | qbcore - https://github.com/qbcore-framework 5 | esx - https://github.com/esx-framework/esx_core 6 | qbox - https://github.com/Qbox-project/qbx-core 7 | standalone - 8 | --]] 9 | framework = 'qbcore', 10 | 11 | language = 'en', 12 | 13 | --[[ 14 | qb-inventory 15 | esx 16 | ox_inventory 17 | ps-inventory 18 | qs-inventory 19 | --]] 20 | 21 | inventory = 'qb-inventory', 22 | 23 | debug = false, -- This just enables debug prints, if having issues with your script, enable this and then check the console and react out to me in the discord 24 | 25 | targetingOptions = { 26 | interaction = 'target', -- 'target' or 'interaction' 27 | 28 | target = 'qb', -- if using target then this is the target system you use, 'ox', 'qb', 'standalone' 29 | }, 30 | 31 | placingBench = { 32 | rotationSpeed = 0.5, -- speed of rotation for placing benches 33 | 34 | leftControl = 44, -- left control to rotate left 35 | 36 | rightControl = 38, -- right control to rotate right 37 | 38 | placeControl = 23, -- control to place the bench 39 | 40 | cancelControl = 120, -- control to cancel placing the bench 41 | 42 | minusOffset = -5.0, -- this is the rotation y, for how it is placed on the ground for the red and green lines 43 | 44 | plusOffset = 5.0, -- this is the rotation y, for how it is placed on the ground for the red and green lines 45 | 46 | limit = 3, -- limit on the amount of benches that can be placed by person, set this to nil if you want to have no limit 47 | }, 48 | 49 | -- just of ox notify, can use your own goto client/notify and server/notify and replace the inside of the function 50 | libText = { 51 | notfiyPoistion = 'center-left', 52 | textUIPosition = 'left-center', 53 | }, 54 | 55 | -- these are the items in which allow you to place your bench down 56 | benchItems = { 57 | {itemName = 'weap_bench', type = 'weapon', object = `gr_prop_gr_bench_04a`}, 58 | {itemName = 'misc_bench', type = 'misc', object = `gr_prop_gr_bench_04a`}, 59 | -- {itemName = 'chicken_bench', type = 'cluckinBell'}, 60 | }, 61 | 62 | prePlacedBenches = { 63 | {location = vector3(-986, -434, 36), rotation = vector3(0, 0, 0), type = 'weapon'}, -- TYPE needs to be one from above as this is where it will index the object from! {location = vector3(x, y, z), rotation = vector3(x, y, z), type = 'weapon'} 64 | }, 65 | 66 | previewBlueprints = true, -- if you want to preview blueprints in the crafting menu 67 | 68 | enableFavourites = true, -- if you want to enable the favourites system 69 | 70 | unlimitedBlueprints = true, -- if you want blueprints to be unlimited uses 71 | 72 | inventoryItemImagesAuto = true, -- if true it will automatically generate the images for the items in the inventory, if false you will need to add the images yourself wtihin the config 73 | } -------------------------------------------------------------------------------- /server/benches/placing.lua: -------------------------------------------------------------------------------- 1 | -- RegisterCommand('placebench', function(source) 2 | -- local customChecks = customChecks(source) 3 | -- local checks = limitChecker(source) 4 | -- Wait(150) 5 | -- if not checks then return end 6 | -- TriggerClientEvent('pure-crafting:placebench', source, 'attachments') 7 | -- end) 8 | 9 | function limitChecker(source) 10 | -- Notify and checks and shit 11 | if not Config.placingBench.limit then return true end 12 | local user = Players[tostring(source)] 13 | local checkUser = true 14 | if not user then 15 | checkUser = checkPerson(source) 16 | end 17 | 18 | if not checkUser then 19 | local uniqueId = getPlayerUniqueId(source) 20 | local id = MySQL.insert.await('INSERT INTO `crafting_users` (uniqueId) VALUES (?)', { 21 | uniqueId 22 | }) 23 | user = User:new(source) 24 | Players[tostring(source)] = user 25 | return true 26 | end 27 | 28 | local amt = Config.placingBench.limit 29 | local myAmt = user.amountPlaced 30 | if myAmt >= amt then 31 | notifySystem(source, { 32 | title = _Lang('placingBench.limitReached'), 33 | type = 'error', 34 | position = Config.libText.notfiyPoistion, 35 | }) 36 | return 37 | end 38 | return true 39 | end 40 | 41 | function serverChecks(source) 42 | local customChecks = customChecks(source) 43 | local checks = limitChecker(source) 44 | Wait(150) 45 | if not checks then return end 46 | return true 47 | end 48 | 49 | function pickupBench(source, benchId) 50 | local bench = ActiveBenches[tostring(benchId)] 51 | if not bench then return end 52 | local uniqueId = getPlayerUniqueId(source) 53 | if uniqueId ~= bench.userPlaced then return end 54 | local type = bench.type 55 | 56 | for i = 1, #Benches do 57 | if Benches[i].id == benchId then 58 | Benches[i] = nil 59 | break 60 | end 61 | end 62 | 63 | 64 | for i = 1, #Config.benchItems do 65 | local benchItem = Config.benchItems[i].type 66 | if benchItem == type then 67 | local item = Config.benchItems[i].itemName 68 | giveItem(source, item, 1) 69 | break 70 | end 71 | end 72 | 73 | local row = MySQL.single.await('SELECT `blueprints` FROM `crafting_benches` WHERE `id` = ? LIMIT 1', { 74 | benchId 75 | }) 76 | 77 | if row then 78 | local blueprints = json.decode(row.blueprints) 79 | for k = 1, #blueprints do local v = blueprints[k] 80 | giveItem(source, v, 1) 81 | end 82 | end 83 | 84 | local user = Players[tostring(source)] 85 | if user then 86 | user.amountPlaced = user.amountPlaced - 1 87 | MySQL.update.await('UPDATE crafting_users SET amountPlaced = ? WHERE uniqueId = ?', { 88 | user.amountPlaced, user.uniqueId 89 | }) 90 | end 91 | 92 | MySQL.query.await('DELETE FROM crafting_benches WHERE id = ?', {benchId}) 93 | ActiveBenches[tostring(benchId)] = nil 94 | TriggerClientEvent('pure-crafting:refreshBenches', -1, Benches) 95 | end 96 | -------------------------------------------------------------------------------- /web/src/components/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from '../../store/store'; 2 | // import { setInMenu } from '../../../store/stores/pages/pages'; 3 | import style from './index.module.css'; 4 | 5 | const ConfirmPopup = () => { 6 | const popup = useAppSelector((state) => state.popup); 7 | const theme = useAppSelector((state) => state.config.theme); 8 | const language = useAppSelector((state) => state.config.language); 9 | 10 | return ( 11 |
{ 14 | if (!e.target || typeof e.target.className !== 'string') return; 15 | if (e.target.className.includes('container')) { 16 | popup.onCancel(); 17 | // dispatch(setInMenu(false)); 18 | } 19 | }}> 20 |
29 |

{popup.popupTitle}

30 |

{popup.popupText}

31 |
36 |
{ 39 | popup.onSubmit(); 40 | }} 41 | onMouseOver={(e) => { 42 | e.currentTarget.style.background = `radial-gradient(50% 50% at 50% 50%, #4ADF47 0%, #158d13 100%)`; 43 | }} 44 | onMouseOut={(e) => { 45 | e.currentTarget.style.background = `radial-gradient(60% 60% at 50% 50%, #4ADF47 0%, #169814 100%)`; 46 | }} 47 | style={{ 48 | background: 49 | 'radial-gradient(60% 60% at 50% 50%, #4ADF47 0%, #169814 100%)', 50 | border: `0.15vw solid rgba(70, 255, 78, 1)`, 51 | color: 'rgba(0, 253, 25, 1)', 52 | }}> 53 | {language.claim} 54 |
55 |
{ 58 | popup.onCancel(); 59 | }} 60 | onMouseOver={(e) => { 61 | e.currentTarget.style.background = `radial-gradient(50% 50% at 50% 50%, #DB3F3F 0%, #a51919 100%)`; 62 | }} 63 | onMouseOut={(e) => { 64 | e.currentTarget.style.background = `radial-gradient(60% 60% at 50% 50%, #DB3F3F 0%, #AA2020 100%)`; 65 | }} 66 | style={{ 67 | background: 68 | 'radial-gradient(60% 60% at 50% 50%, #DB3F3F 0%, #AA2020 100%)', 69 | border: `0.15vw solid rgba(234, 47, 47, 1)`, 70 | color: 'rgba(255, 61, 61, 1)', 71 | }}> 72 | {language.cancel} 73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default ConfirmPopup; 81 | -------------------------------------------------------------------------------- /server/modules/events.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent('pure-crafting:exitZone', function(benchId) 2 | local bench = ActiveBenches[tostring(benchId)] 3 | if not bench then return end 4 | bench:removePlayer(source) 5 | end) 6 | 7 | RegisterNetEvent('pure-crafting:enterZone', function(benchId) 8 | local bench = ActiveBenches[tostring(benchId)] 9 | if not bench then 10 | print('bench not found', benchId, source) 11 | return 12 | end 13 | bench:addPlayer(source) 14 | end) 15 | 16 | RegisterNetEvent('pure-crafting:playerLoaded', function() 17 | local src = source 18 | sendBenches(src) 19 | initUser(src) 20 | end) 21 | 22 | RegisterNetEvent('pure-crafting:playerUnloaded', function() 23 | local src = source 24 | removeUser(src) 25 | end) 26 | 27 | RegisterNetEvent('pure-crafting:blueprintUsed', function(benchId, name) 28 | local src = source 29 | local bench = ActiveBenches[tostring(benchId)] 30 | if not bench then return end 31 | bench:useBlueprint(src, name) 32 | end) 33 | 34 | RegisterNetEvent('pure-crafting:craftFinished', function(benchId, data) 35 | local bench = ActiveBenches[tostring(benchId)] 36 | if not bench then return end 37 | bench:finishedCraft(data) 38 | end) 39 | 40 | RegisterNetEvent('pure-crafting:claimCraft', function(benchId, data) 41 | local bench = ActiveBenches[tostring(benchId)] 42 | if not bench then return end 43 | bench:claimCraft(data.id, data.index, source) 44 | end) 45 | 46 | RegisterNetEvent('pure-crafting:cancelCraft', function(benchId, data) 47 | local bench = ActiveBenches[tostring(benchId)] 48 | if not bench then return end 49 | bench:cancelCraft(data.id, data.index, source) 50 | end) 51 | 52 | RegisterNetEvent('pure-crafting:attemptCraft', function(benchId, data) 53 | local amount, currentItem, item = data.amount, data.currentItem, data.item 54 | local bench = ActiveBenches[tostring(benchId)] 55 | if not bench then 56 | debugPrint('pure-crafting:attemptCraft | failed - no bench', source) 57 | return 58 | end 59 | local expectedItem = getItemFromId(tostring(currentItem), tostring(bench.type)) 60 | if not expectedItem then 61 | debugPrint('pure-crafting:attemptCraft | failed - no expected item', json.encode(expectedItem), json.encode(item)) 62 | return 63 | end 64 | if expectedItem.name ~= item.name then 65 | debugPrint('pure-crafting:attemptCraft | failed - item names dont match', json.encode(expectedItem), json.encode(item)) 66 | return 67 | end 68 | bench:craftItem(amount, item, source) 69 | end) 70 | 71 | RegisterNetEvent('pure-crafting:setFavourite', function(data) 72 | local itemName = data.itemName 73 | local src = source 74 | local user = Players[tostring(source)] 75 | local checkUser = true 76 | 77 | if not user then 78 | checkUser = checkPerson(source) 79 | end 80 | 81 | if not checkUser then 82 | createUserSetFave(src, itemName) 83 | return 84 | end 85 | 86 | if not user then 87 | print('no user???') 88 | return 89 | end 90 | 91 | if user:checkFave(itemName) then 92 | user:removeFave(itemName) 93 | else 94 | user:addFave(itemName) 95 | end 96 | end) -------------------------------------------------------------------------------- /web/src/components/Info/Queue/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useAppDistpatch, useAppSelector } from '../../../store/store'; 3 | import style from './index.module.css'; 4 | import { sendNui } from '../../../utils/sendNui'; 5 | import { hidePopup, setPopup } from '../../../store/stores/popup/popup'; 6 | 7 | interface Props { 8 | image: string; 9 | secondsLeft: number; 10 | timeStarted: number; 11 | startTimer: boolean; 12 | finished: boolean; 13 | id: number; 14 | index: number; 15 | } 16 | 17 | const Queue = (props: Props) => { 18 | const theme = useAppSelector((state) => state.config.theme); 19 | const language = useAppSelector((state) => state.config.language); 20 | const [hovering, setHovering] = useState(false); 21 | const dispatch = useAppDistpatch(); 22 | 23 | if (props.finished) { 24 | return ( 25 |
{ 28 | dispatch( 29 | setPopup({ 30 | showPopup: true, 31 | popupTitle: language.claimCraft, 32 | popupText: language.areYouSure, 33 | onSubmit: () => { 34 | sendNui('claimCraft', { 35 | id: props.id, 36 | index: props.index, 37 | }); 38 | dispatch(hidePopup()); 39 | }, 40 | onCancel: () => { 41 | dispatch(hidePopup()); 42 | }, 43 | }) 44 | ); 45 | }} 46 | style={{ 47 | background: theme.greenFaded, 48 | border: `0.2vw solid ${theme.border}`, 49 | }}> 50 | 51 |

56 | {language.claim} 57 |

58 |
59 | ); 60 | } 61 | 62 | return ( 63 |
{ 66 | dispatch( 67 | setPopup({ 68 | showPopup: true, 69 | popupTitle: language.cancelCraft, 70 | popupText: language.areYouSure, 71 | onSubmit: () => { 72 | sendNui('cancelCraft', { 73 | id: props.id, 74 | index: props.index, 75 | }); 76 | dispatch(hidePopup()); 77 | }, 78 | onCancel: () => { 79 | dispatch(hidePopup()); 80 | }, 81 | }) 82 | ); 83 | }} 84 | onMouseOver={() => { 85 | setHovering(true); 86 | }} 87 | onMouseLeave={() => { 88 | setHovering(false); 89 | }} 90 | style={{ 91 | background: hovering ? theme.redFaded : theme.main, 92 | border: `0.2vw solid ${theme.border}`, 93 | }}> 94 | 95 |

100 | {hovering ? language.cancel : props.secondsLeft + language.s} 101 |

102 |
103 | ); 104 | }; 105 | 106 | export default Queue; 107 | -------------------------------------------------------------------------------- /web/src/components/Info/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 69vh; 5 | width: 35.9vw; 6 | padding: 0 2.2vw; 7 | box-shadow: rgb(0, 0, 0, 0.35) 0px 0px 16px; 8 | } 9 | 10 | .header { 11 | width: 100%; 12 | height: 23vh; 13 | display: flex; 14 | justify-content: flex-start; 15 | align-items: center; 16 | gap: 1vw; 17 | } 18 | 19 | .imgHousing { 20 | height: 14.8vh; 21 | width: 8.4vw; 22 | margin-top: 0.4vh; 23 | /* box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05) 0px 0px 6px; */ 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .textHousing { 30 | height: 15vh; 31 | width: 21vw; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: center; 35 | gap: 0.6vw; 36 | } 37 | 38 | .img { 39 | width: 7vw; 40 | height: 7vw; 41 | } 42 | 43 | .heading { 44 | font-size: 1.9vw; 45 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 46 | 0px 4px 7px rgba(0, 0, 0, 0.25); 47 | font-family: 'Black'; 48 | margin-left: -0.18vh; 49 | height: 4vh; 50 | } 51 | 52 | .description { 53 | font-size: 1.12vw; 54 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 55 | 0px 4px 7px rgba(0, 0, 0, 0.25); 56 | line-height: 1; 57 | } 58 | 59 | .flex { 60 | display: flex; 61 | flex-direction: row; 62 | align-items: center; 63 | gap: 0.8vw; 64 | } 65 | 66 | .icon { 67 | font-size: 1.2vw; 68 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.45)); 69 | } 70 | 71 | .smallText { 72 | font-size: 1.1vw; 73 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 74 | 0px 4px 7px rgba(0, 0, 0, 0.25); 75 | margin-top: -0.4vh; 76 | } 77 | 78 | .middle { 79 | width: 100%; 80 | height: 30vh; 81 | /* background-color: rgba(200, 255, 0, 0.414); */ 82 | } 83 | 84 | .spacer { 85 | width: 100%; 86 | height: 2.7vh; 87 | } 88 | 89 | .queue { 90 | width: 100%; 91 | height: 17vh; 92 | display: flex; 93 | justify-content: flex-start; 94 | align-items: center; 95 | flex-direction: row; 96 | gap: 2.4vw; 97 | overflow-y: auto; 98 | padding: 0 2vw; 99 | } 100 | 101 | .bottom { 102 | width: 100%; 103 | height: 9.3vh; 104 | display: flex; 105 | justify-content: flex-start; 106 | align-items: center; 107 | flex-direction: row; 108 | gap: 1.2vw; 109 | } 110 | 111 | .fave { 112 | width: 2.8vw; 113 | height: 5vh; 114 | position: absolute; 115 | margin-left: 28.3vw; 116 | margin-top: -10vh; 117 | display: flex; 118 | justify-content: center; 119 | align-items: center; 120 | cursor: pointer; 121 | overflow: hidden; 122 | } 123 | 124 | .star { 125 | font-size: 1.05vw; 126 | filter: drop-shadow(2px 2px 6px rgba(0, 0, 0, 0.45)); 127 | } 128 | 129 | .text { 130 | font-size: 1.5vw; 131 | text-shadow: 3px 0px 7px rgba(0, 0, 0, 0.25), -3px 0px 7px rgba(0, 0, 0, 0.25), 132 | 0px 4px 7px rgba(0, 0, 0, 0.25); 133 | font-family: 'Black'; 134 | width: 31vw; 135 | white-space: nowrap; 136 | text-overflow: ellipsis; 137 | overflow: hidden; 138 | position: absolute; 139 | padding-left: 0.75vw; 140 | padding-top: 0.7vh; 141 | } 142 | -------------------------------------------------------------------------------- /shared/functions.lua: -------------------------------------------------------------------------------- 1 | local currentResourceName = GetCurrentResourceName() 2 | CraftableItems = {} 3 | Blueprints = {} 4 | BlueprintsToSend = {} 5 | local takenIds = {} 6 | 7 | function debugPrint(...) 8 | if not Config.debug then return end 9 | local args = { ... } 10 | 11 | local appendStr = '' 12 | for _, v in ipairs(args) do 13 | appendStr = appendStr .. ' ' .. tostring(v) 14 | end 15 | local msgTemplate = '^3[%s] | [%s] | ^0%s' 16 | local finalMsg = msgTemplate:format(currentResourceName, GetInvokingResource(), appendStr) 17 | print(finalMsg) 18 | end 19 | 20 | function getItemFromId(id, type) 21 | local item = CraftableItems[type][id] 22 | if not item then 23 | debugPrint('getItemFromId | failed - no item', id) 24 | return 25 | end 26 | return item 27 | end 28 | 29 | function getBlueprintsFromType(type) 30 | if not Config.previewBlueprints then 31 | return {} 32 | end 33 | local items = BlueprintsToSend[type] 34 | if not items then 35 | debugPrint('getBlueprintsFromType | failed - no items', type) 36 | return {} 37 | end 38 | return items 39 | end 40 | 41 | function getBlueprintFromId(id, type) 42 | local item = Blueprints[type][id] 43 | if not item then 44 | debugPrint('getBlueprintFromId | failed - no item', id) 45 | return 46 | end 47 | return item 48 | end 49 | 50 | function canUseBlueprint(type, id) 51 | local item = Blueprints[type][id] 52 | if not item then 53 | debugPrint('canUseBlueprint | failed - no item', id) 54 | return false 55 | end 56 | return true 57 | end 58 | 59 | function initCraftables() 60 | for k, v in pairs(Config.items) do 61 | CraftableItems[tostring(k)] = {} 62 | for i = 1, #v do 63 | local item = v[i] 64 | if not item.id then return end 65 | CraftableItems[tostring(k)][tostring(item.id)] = item 66 | takenIds[tostring(item.id)] = true 67 | end 68 | end 69 | 70 | for k, v in pairs(Config.blueprints.ids) do 71 | Blueprints[tostring(k)] = {} 72 | for i = 1, #v do 73 | local item = v[i] 74 | if not item.id then return end 75 | CraftableItems[tostring(k)][tostring(item.id)] = item 76 | Blueprints[tostring(k)][tostring(item.blueprintId)] = item 77 | takenIds[tostring(item.id)] = true 78 | end 79 | end 80 | 81 | for k, v in pairs(Config.blueprints.ids) do 82 | BlueprintsToSend[tostring(k)] = {} 83 | for i = 1, #v do 84 | local item = v[i] 85 | if not item.id then return end 86 | local newId = math.random(100, 99999) 87 | while takenIds[tostring(newId)] do 88 | newId = math.random(100, 99999) 89 | end 90 | local newTble = { 91 | id = newId, 92 | blueprintId = item.blueprintId, 93 | itemName = item.itemName, 94 | name = item.name, 95 | image = item.image, 96 | category = item.category, 97 | type = item.type, 98 | description = item.description, 99 | craftingTime = item.craftingTime, 100 | requiredItems = {} 101 | } 102 | BlueprintsToSend[tostring(k)][#BlueprintsToSend[tostring(k)] + 1] = newTble 103 | end 104 | end 105 | end 106 | 107 | CreateThread(function() 108 | initCraftables() 109 | end) -------------------------------------------------------------------------------- /server/benches/initiate.lua: -------------------------------------------------------------------------------- 1 | Benches = {} 2 | 3 | function getBenches() 4 | debugPrint('getBenches | Getting benches from database.') 5 | local response = MySQL.query.await('SELECT * FROM crafting_benches') 6 | if not response then return end 7 | 8 | local newTable = {} 9 | 10 | for i = 1, #response do 11 | local row = response[i] 12 | -- local location = json.decode(row.location) 13 | -- local rotation = json.decode(row.rotation) 14 | newTable[#newTable + 1] = { 15 | id = row.id, 16 | location = row.location, 17 | rotation = row.rotation, 18 | queue = row.queue, 19 | finished = row.finished, 20 | userPlaced = row.userPlaced, 21 | type = row.type, 22 | obj = nil 23 | } 24 | initQueue(row.id, row.queue, row.finished, row.blueprints, row.type, row.userPlaced) 25 | end 26 | 27 | if not Config.prePlacedBenches then goto continue end 28 | for i = 1, #Config.prePlacedBenches do 29 | local bench = Config.prePlacedBenches[i] 30 | local benchId = 9999999 + i 31 | newTable[#newTable + 1] = { 32 | id = benchId, 33 | location = json.encode(bench.location), 34 | rotation = json.encode(bench.rotation), 35 | queue = json.encode({}), 36 | finished = json.encode({}), 37 | userPlaced = 'preplaced', 38 | type = bench.type, 39 | obj = nil 40 | } 41 | initQueue(benchId, json.encode({}), json.encode({}), json.encode({}), bench.type, 'preplaced') 42 | end 43 | 44 | ::continue:: 45 | 46 | Benches = newTable 47 | 48 | debugPrint('getBenches | Benches:', json.encode(Benches)) 49 | end 50 | 51 | 52 | function insertBench(location, rotation, source, type) 53 | local uniqueId = getPlayerUniqueId(source) 54 | local id = MySQL.insert.await('INSERT INTO `crafting_benches` (location, rotation, type, userPlaced) VALUES (?, ?, ?, ?)', { 55 | json.encode(location), json.encode(rotation), type, uniqueId 56 | }) 57 | 58 | local newBench = { 59 | id = id, 60 | location = location, 61 | rotation = rotation, 62 | queue = json.encode({}), 63 | finished = json.encode({}), 64 | userPlaced = uniqueId, 65 | type = type, 66 | obj = nil 67 | } 68 | Benches[#Benches + 1] = newBench 69 | 70 | TriggerClientEvent('pure-crafting:insertBench', -1, newBench) 71 | initQueue(id, json.encode({}), json.encode({}), json.encode({}), type, uniqueId) 72 | for i = 1, #Config.benchItems do 73 | local benchItem = Config.benchItems[i].type 74 | if benchItem == type then 75 | local item = Config.benchItems[i].itemName 76 | removeItem(source, item, 1) 77 | break 78 | end 79 | end 80 | 81 | local user = Players[tostring(source)] 82 | if not user then return end 83 | user.amountPlaced = user.amountPlaced + 1 84 | MySQL.update.await('UPDATE crafting_users SET amountPlaced = ? WHERE uniqueId = ?', { 85 | user.amountPlaced, user.uniqueId 86 | }) 87 | end 88 | 89 | RegisterCommand('benches', function(source) 90 | getBenches() 91 | sendBenches(source) 92 | initUser(source) 93 | end) 94 | 95 | function sendBenches(source) 96 | TriggerClientEvent('pure-crafting:refreshBenches', source, Benches) 97 | end 98 | 99 | CreateThread(function() 100 | getBenches() 101 | end) -------------------------------------------------------------------------------- /server/blueprints/blueprints.lua: -------------------------------------------------------------------------------- 1 | function Queue:useBlueprint(source, name) 2 | local canUseBlueprintOnBench = canUseBlueprint(self.type, name) 3 | if not canUseBlueprintOnBench then 4 | notifySystem(source, { 5 | title = _Lang('blueprints.cantUse'), 6 | type = 'error', 7 | position = Config.libText.notfiyPoistion, 8 | }) 9 | return 10 | end 11 | for i = 1, #self.blueprints do 12 | if self.blueprints[i] == name then 13 | notifySystem(source, { 14 | title = _Lang('blueprints.alreadyUsed'), 15 | type = 'error', 16 | position = Config.libText.notfiyPoistion, 17 | }) 18 | return 19 | end 20 | end 21 | self.blueprints[#self.blueprints + 1] = name 22 | self.bpHash[name] = true 23 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET blueprints = ? WHERE id = ?', { 24 | json.encode(self.blueprints), self.benchId 25 | }) 26 | self:triggerEvent('pure-crafting:generateItems', self.benchId) 27 | notifySystem(source, { 28 | title = _Lang('blueprints.added'), 29 | type = 'success', 30 | position = Config.libText.notfiyPoistion, 31 | }) 32 | removeItem(source, name, 1) 33 | debugPrint('Queue:useBlueprint | ', json.encode(self.blueprints)) 34 | end 35 | 36 | function Queue:removeBlueprint(source, name) 37 | for i = 1, #self.blueprints do 38 | if self.blueprints[i] == name then 39 | table.remove(self.blueprints, i) 40 | self.bpHash[name] = nil 41 | local affectedRows = MySQL.update.await('UPDATE crafting_benches SET blueprints = ? WHERE id = ?', { 42 | json.encode(self.blueprints), self.benchId 43 | }) 44 | self:triggerEvent('pure-crafting:generateItems', self.benchId) 45 | debugPrint('Queue:removeBlueprint | ', json.encode(self.blueprints)) 46 | return 47 | end 48 | end 49 | return 50 | end 51 | 52 | function Queue:triggerEvent(eventName, ...) 53 | for i = 1, #self.activeMembers do 54 | debugPrint('Queue:triggerEvent | Triggering event: ', eventName, self.activeMembers[i], ...) 55 | TriggerClientEvent(eventName, self.activeMembers[i], ...) 56 | end 57 | end 58 | 59 | function Queue:removePlayer(source) 60 | for i = 1, #self.activeMembers do 61 | if self.activeMembers[i] == source then 62 | table.remove(self.activeMembers, i) 63 | break 64 | end 65 | end 66 | debugPrint('Queue:removePlayer | benchId: ', self.benchId, ' | sources online: ', json.encode(self.activeMembers)) 67 | end 68 | 69 | function Queue:addPlayer(source) 70 | self.activeMembers[#self.activeMembers + 1] = source 71 | debugPrint('Queue:addPlayer | benchId: ', self.benchId, ' | sources online: ', json.encode(self.activeMembers)) 72 | end 73 | 74 | CreateThread(function() 75 | createItems() 76 | while true do 77 | for k,v in pairs(ActiveBenches) do 78 | local bench = ActiveBenches[k] 79 | if not bench.items[1] then 80 | goto continue 81 | end 82 | local item = bench.items[1] 83 | item.secondsLeft = item.secondsLeft - 1 84 | bench:triggerEvent('pure-crafting:secondChange', bench.items[1].secondsLeft) 85 | if item.secondsLeft <= 0 then 86 | bench:finishedCraft(item) 87 | end 88 | ::continue:: 89 | end 90 | Wait(1000) 91 | end 92 | end) 93 | 94 | function createItems() 95 | for i = 1, #Config.blueprints.items do 96 | createItem(Config.blueprints.items[i], 'pure-crafting:useBlueprint', Config.blueprints.items[i]) 97 | end 98 | 99 | for i = 1, #Config.benchItems do 100 | createItem(Config.benchItems[i].itemName, 'pure-crafting:beforeBenches', Config.benchItems[i].type) 101 | end 102 | end 103 | 104 | function getRandomBlueprint() 105 | local random = math.random(1, #Config.blueprints.items) 106 | return Config.blueprints.items[random] 107 | end 108 | 109 | exports('getRandomBlueprint', getRandomBlueprint) -------------------------------------------------------------------------------- /web/src/components/Info/Bottom/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppDistpatch, useAppSelector } from '../../../store/store'; 2 | import style from './index.module.css'; 3 | import { sendNui } from '../../../utils/sendNui'; 4 | import { useEffect } from 'react'; 5 | import updateRipples from '../../../utils/updateRipples'; 6 | import { setAmount } from '../../../store/stores/crafting/crafting'; 7 | 8 | const Bottom = () => { 9 | const theme = useAppSelector((state) => state.config.theme); 10 | const language = useAppSelector((state) => state.config.language); 11 | const number = useAppSelector((state) => state.crafting.amount); 12 | const currentItem = useAppSelector((state) => state.crafting.selectedItem); 13 | const item = useAppSelector((state) => state.crafting.currentItem); 14 | const dispatch = useAppDistpatch(); 15 | 16 | const setNumber = (int: number) => { 17 | dispatch(setAmount(int)); 18 | }; 19 | 20 | const canCraft = () => { 21 | let canCraft = true; 22 | if (!item) { 23 | return false; 24 | } 25 | item.requiredItems.forEach((requiredItem) => { 26 | const hasEnough = 27 | requiredItem.myAmount - requiredItem.amount * number >= 0; 28 | if (!hasEnough) { 29 | canCraft = false; 30 | } 31 | }); 32 | return canCraft; 33 | }; 34 | 35 | const changeNumber = (int: number) => { 36 | if (int < 0) { 37 | if (number - 1 <= 0) { 38 | setNumber(1); 39 | return; 40 | } 41 | setNumber(number - 1); 42 | return; 43 | } 44 | setNumber(number + 1); 45 | }; 46 | 47 | useEffect(() => { 48 | updateRipples(); 49 | }); 50 | 51 | return ( 52 | <> 53 |
54 |
{ 58 | if (item && item.type === 'blueprint') { 59 | return; 60 | } 61 | changeNumber(-1); 62 | }} 63 | style={{ 64 | background: theme.button, 65 | border: `0.2vw solid ${theme.border}`, 66 | borderRight: 'none', 67 | color: theme.white, 68 | }}> 69 |

-

70 |
71 | { 75 | if (item && item.type === 'blueprint') { 76 | return; 77 | } 78 | setNumber(parseInt(e.target.value)); 79 | }} 80 | className={style.input} 81 | style={{ 82 | background: theme.main, 83 | border: `0.2vw solid ${theme.border}`, 84 | color: theme.white, 85 | }} 86 | /> 87 |
{ 91 | if (item && item.type === 'blueprint') { 92 | return; 93 | } 94 | changeNumber(1); 95 | }} 96 | style={{ 97 | background: theme.button, 98 | border: `0.2vw solid ${theme.border}`, 99 | borderLeft: 'none', 100 | color: theme.white, 101 | }}> 102 |

+

103 |
104 |
105 |
{ 109 | if (!item) { 110 | return; 111 | } 112 | const craftingSuccess = canCraft(); 113 | if (!craftingSuccess) { 114 | sendNui('notEnoughItems', {}); 115 | return; 116 | } 117 | sendNui('attemptCraft', { 118 | amount: number, 119 | currentItem: currentItem, 120 | item: item, 121 | }); 122 | }} 123 | style={{ 124 | background: theme.button, 125 | border: `0.2vw solid ${theme.border}`, 126 | color: theme.white, 127 | }}> 128 | {language.craft} 129 |
130 | 131 | ); 132 | }; 133 | 134 | export default Bottom; 135 | -------------------------------------------------------------------------------- /client/benches/setup.lua: -------------------------------------------------------------------------------- 1 | Benches = {} 2 | 3 | RegisterNetEvent('pure-crafting:refreshBenches', function(benches) 4 | local src = source 5 | removeZones() 6 | for i = 1, #Benches do 7 | removeZone('bench_'.. Benches[i].id) 8 | end 9 | deleteAllBenches() 10 | Benches = benches 11 | setupBenches(src) 12 | end) 13 | 14 | function setupBenches(source) 15 | local uniqueId = getPlayerUniqueId(source) 16 | for i = 1, #Benches do 17 | local bench = Benches[i] 18 | local location, rotation = json.decode(bench.location), json.decode(bench.rotation) 19 | local tableForTarget = { 20 | options = { 21 | { 22 | type = 'client', 23 | action = function() 24 | openCrafting(source, bench.id) 25 | end, 26 | icon = 'fas fa-wrench', 27 | label = 'Use Bench', 28 | canInteract = function() 29 | return true 30 | end 31 | }, 32 | { 33 | type = 'client', 34 | action = function() 35 | pickupBench(source, bench.id) 36 | end, 37 | icon = 'fas fa-hand', 38 | label = 'Pickup Bench', 39 | canInteract = function() 40 | return uniqueId == bench.userPlaced 41 | end 42 | } 43 | }, 44 | distance = 1.0, 45 | location = location, 46 | } 47 | 48 | if Config.targetingOptions.interaction == 'target' then 49 | local name = 'bench_'.. bench.id 50 | addTargetToCoords(location, vector3(2, 2, 2), tableForTarget, name) 51 | end 52 | 53 | setupBenchZone(location, rotation, vector3(3, 3, 4)) 54 | 55 | local benchObj = createBench(location, rotation, bench.type) 56 | bench.obj = benchObj 57 | end 58 | end 59 | 60 | RegisterNetEvent('pure-crafting:insertBench', function(newBench) 61 | insertBench(source, newBench) 62 | end) 63 | 64 | function insertBench(source, newBench) 65 | local uniqueId = getPlayerUniqueId(source) 66 | local location, rotation = newBench.location, newBench.rotation 67 | local tableForTarget = { 68 | options = { 69 | { 70 | type = 'client', 71 | action = function() 72 | openCrafting(source, newBench.id) 73 | end, 74 | icon = 'fas fa-wrench', 75 | label = 'Use Bench', 76 | canInteract = function() 77 | return true 78 | end 79 | }, 80 | { 81 | type = 'client', 82 | action = function() 83 | pickupBench(source, newBench.id) 84 | end, 85 | icon = 'fas fa-hand', 86 | label = 'Pickup Bench', 87 | canInteract = function() 88 | return uniqueId == newBench.userPlaced 89 | end 90 | } 91 | }, 92 | distance = 1.0, 93 | location = location, 94 | } 95 | 96 | if Config.targetingOptions.interaction == 'target' then 97 | local name = 'bench_'.. newBench.id 98 | addTargetToCoords(location, vector3(2, 2, 2), tableForTarget, name) 99 | end 100 | 101 | setupBenchZone(location, rotation, vector3(3, 3, 4)) 102 | 103 | local benchObj = createBench(location, rotation, newBench.type) 104 | newBench.obj = benchObj 105 | Benches[#Benches + 1] = newBench 106 | end 107 | 108 | RegisterNetEvent('pure-crafting:beforeBenches', function(data) 109 | local serverChecks = lib.callback.await('pure-crafting:serverChecks', false, data) 110 | if not serverChecks then return end 111 | placeBench(source, data) 112 | end) 113 | 114 | function generateObjFromType(type) 115 | for i = 1, #Config.benchItems do 116 | local benchItem = Config.benchItems[i].type 117 | if benchItem == type then 118 | return Config.benchItems[i].object 119 | end 120 | end 121 | return `prop_tool_bench` 122 | end -------------------------------------------------------------------------------- /server/inventory/psInventory.lua: -------------------------------------------------------------------------------- 1 | if Config.inventory ~= 'ps-inventory' then return end 2 | local QBCore = exports['qb-core']:GetCoreObject() 3 | 4 | function removeItem(source, item, amount) 5 | return exports['ps-inventory']:RemoveItem(source, item, amount) 6 | end 7 | 8 | function checkItem(source, item, amount) 9 | return exports['ps-inventory']:HasItem(source, item, amount) 10 | end 11 | 12 | function getItems(source, item) 13 | return exports['ps-inventory']:GetItemByName(source, item) 14 | end 15 | 16 | function giveItem(source, item, amount) 17 | exports['ps-inventory']:AddItem(source, item, amount) 18 | end 19 | 20 | function createItem(name, trigger, data) 21 | QBCore.Functions.CreateUseableItem(name, function(source, item) 22 | TriggerClientEvent(trigger, source, data) 23 | end) 24 | end 25 | 26 | print('PS Inventory Is Untested, Please Report Any Issues') 27 | 28 | -- THESE ARE EXAMPLES OF BLUEPRINTS / FOLLOW THE DOCS FOR MORE INFORMATION 29 | 30 | -- -- Crafting 31 | -- weap_bench = {name = 'weap_bench', label = 'Weapons Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Weapons Bench'}, 32 | -- misc_bench = {name = 'misc_bench', label = 'Misc Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Misc Bench'}, 33 | 34 | -- -- Blueprints 35 | -- blueprint_molotov = {name = 'blueprint_molotov', label = 'Molotov Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Molotov Blueprint'}, 36 | -- blueprint_grip = {name = 'blueprint_grip', label = 'Grip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Grip Blueprint'}, 37 | -- blueprint_suppressor = {name = 'blueprint_suppressor', label = 'Suppressor Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Suppressor Blueprint'}, 38 | -- blueprint_extendedclip = {name = 'blueprint_extendedclip', label = 'Extended Clip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Extended Clip Blueprint'}, 39 | -- blueprint_scope = {name = 'blueprint_scope', label = 'Scope Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Scope Blueprint'}, 40 | -- blueprint_specialcarbine = {name = 'blueprint_specialcarbine', label = 'Special Carbine Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Special Carbine Blueprint'}, 41 | -- blueprint_assaultrifle = {name = 'blueprint_assaultrifle', label = 'Assault Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Assault Rifle Blueprint'}, 42 | -- blueprint_advancedrifle = {name = 'blueprint_advancedrifle', label = 'Advanced Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Advanced Rifle Blueprint'}, 43 | -- blueprint_sawnoffshotgun = {name = 'blueprint_sawnoffshotgun', label = 'Sawn Off Shotgun Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Sawn Off Shotgun Blueprint'}, 44 | -- blueprint_machinepistol = {name = 'blueprint_machinepistol', label = 'Machine Pistol Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Machien Pistol Blueprint'}, 45 | -- blueprint_microsmg = {name = 'blueprint_microsmg', label = 'Microsmg Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Microsmg Blueprint'}, 46 | -------------------------------------------------------------------------------- /server/inventory/qsInventory.lua: -------------------------------------------------------------------------------- 1 | if Config.inventory ~= 'qs-inventory' then return end 2 | 3 | function removeItem(source, item, amount) 4 | return exports['qs-inventory']:RemoveItem(source, item, amount) 5 | end 6 | 7 | function checkItem(source, item, amount) 8 | local qsAmount = exports['qs-inventory']:GetItemTotalAmount(source, item) 9 | if qsAmount >= amount then return true end 10 | return false 11 | end 12 | 13 | function getItems(source, item) 14 | return exports['qb-inventory']:GetItemTotalAmount(source, item) 15 | end 16 | 17 | function giveItem(source, item, amount) 18 | return exports['qs-inventory']:AddItem(source, item, amount) 19 | end 20 | 21 | function createItem(name, trigger, data) 22 | exports['qs-inventory']:CreateUsableItem(name, function(source, item) 23 | TriggerClientEvent(trigger, source, data) 24 | end) 25 | end 26 | 27 | print('Quasar Is Untested, Please Report Any Issues') 28 | 29 | -- THESE ARE EXAMPLES OF BLUEPRINTS / FOLLOW THE DOCS FOR MORE INFORMATION 30 | 31 | -- -- Crafting 32 | -- weap_bench = {name = 'weap_bench', label = 'Weapons Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Weapons Bench'}, 33 | -- misc_bench = {name = 'misc_bench', label = 'Misc Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Misc Bench'}, 34 | 35 | -- -- Blueprints 36 | -- blueprint_molotov = {name = 'blueprint_molotov', label = 'Molotov Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Molotov Blueprint'}, 37 | -- blueprint_grip = {name = 'blueprint_grip', label = 'Grip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Grip Blueprint'}, 38 | -- blueprint_suppressor = {name = 'blueprint_suppressor', label = 'Suppressor Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Suppressor Blueprint'}, 39 | -- blueprint_extendedclip = {name = 'blueprint_extendedclip', label = 'Extended Clip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Extended Clip Blueprint'}, 40 | -- blueprint_scope = {name = 'blueprint_scope', label = 'Scope Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Scope Blueprint'}, 41 | -- blueprint_specialcarbine = {name = 'blueprint_specialcarbine', label = 'Special Carbine Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Special Carbine Blueprint'}, 42 | -- blueprint_assaultrifle = {name = 'blueprint_assaultrifle', label = 'Assault Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Assault Rifle Blueprint'}, 43 | -- blueprint_advancedrifle = {name = 'blueprint_advancedrifle', label = 'Advanced Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Advanced Rifle Blueprint'}, 44 | -- blueprint_sawnoffshotgun = {name = 'blueprint_sawnoffshotgun', label = 'Sawn Off Shotgun Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Sawn Off Shotgun Blueprint'}, 45 | -- blueprint_machinepistol = {name = 'blueprint_machinepistol', label = 'Machine Pistol Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Machien Pistol Blueprint'}, 46 | -- blueprint_microsmg = {name = 'blueprint_microsmg', label = 'Microsmg Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Microsmg Blueprint'}, 47 | -------------------------------------------------------------------------------- /client/benches/placing.lua: -------------------------------------------------------------------------------- 1 | local function placingBench(type) 2 | local model = generateObjFromType(type) 3 | RequestModel(model) 4 | local _, _, endCoords, surfaceNormal = raycast() 5 | local rot = normalToRotation(surfaceNormal) 6 | local object = CreateObject(model, endCoords.x, endCoords.y, endCoords.z, false, false, false) 7 | local canPlace = false 8 | 9 | SetEntityAlpha(object, 200) 10 | SetEntityRotation(object, rot.x, rot.y, rot.z, 1) 11 | DisableCamCollisionForEntity(object) 12 | SetEntityCollision(object, false, false) 13 | SetEntityDrawOutlineColor(10, 170, 210, 200) 14 | SetEntityDrawOutlineShader(1) 15 | SetEntityDrawOutline(object, true) 16 | 17 | local zRotation = rot.z 18 | local leftRotation = false 19 | local rightRotation = false 20 | 21 | local text = _Lang('placingBench.left') .. ' | ' .. _Lang('placingBench.right') .. ' | ' .. _Lang('placingBench.place') .. ' | ' .. _Lang('placingBench.cancel') 22 | lib.showTextUI(text) 23 | 24 | while true do 25 | DisableControlAction(0, Config.placingBench.leftControl, true) 26 | DisableControlAction(0, Config.placingBench.rightControl, true) 27 | DisableControlAction(0, Config.placingBench.placeControl, true) 28 | DisableControlAction(0, Config.placingBench.cancelControl, true) 29 | 30 | _, _, endCoords, surfaceNormal = raycast() 31 | rot = normalToRotation(surfaceNormal) 32 | 33 | if (rot.y < Config.placingBench.minusOffset) or (rot.y > Config.placingBench.plusOffset) then 34 | SetEntityDrawOutlineColor(255, 0, 0, 200) 35 | canPlace = false 36 | else 37 | SetEntityDrawOutlineColor(0, 255, 0, 200) 38 | canPlace = true 39 | end 40 | 41 | if IsDisabledControlJustPressed(0, Config.placingBench.leftControl) then 42 | leftRotation = true 43 | end 44 | if IsDisabledControlJustReleased(0, Config.placingBench.leftControl) then 45 | leftRotation = false 46 | end 47 | 48 | if leftRotation then 49 | zRotation = zRotation - Config.placingBench.rotationSpeed 50 | end 51 | 52 | if IsDisabledControlJustPressed(0, Config.placingBench.rightControl) then 53 | rightRotation = true 54 | end 55 | if IsDisabledControlJustReleased(0, Config.placingBench.rightControl) then 56 | rightRotation = false 57 | end 58 | 59 | if rightRotation then 60 | zRotation = zRotation + Config.placingBench.rotationSpeed 61 | end 62 | 63 | SetEntityCoords(object, endCoords) 64 | SetEntityRotation(object, rot.x, rot.y, zRotation, 1) 65 | 66 | if IsDisabledControlJustPressed(0, Config.placingBench.cancelControl) then 67 | DeleteObject(object) 68 | lib.hideTextUI() 69 | return 70 | end 71 | 72 | if IsDisabledControlJustPressed(0, Config.placingBench.placeControl) then 73 | lib.hideTextUI() 74 | local checkZone = checkZone(GetEntityCoords(cache.ped)) 75 | if checkZone then 76 | notifySystem({ 77 | title = _Lang('placingBench.inAnotherZone'), 78 | type = 'error', 79 | position = Config.libText.notfiyPoistion, 80 | }) 81 | DeleteObject(object) 82 | break 83 | end 84 | if not canPlace then 85 | -- DeleteObject(object) 86 | -- break 87 | else 88 | DeleteObject(object) 89 | local success = lib.callback.await('pure-crafting:createBench', false, endCoords, vector3(rot.x, rot.y, zRotation), type) 90 | return 91 | end 92 | end 93 | end 94 | end 95 | 96 | RegisterNetEvent('pure-crafting:placebench', function(type) 97 | placeBench(source, type) 98 | end) 99 | 100 | function placeBench(source, type) 101 | local customChecks = customChecks(source) 102 | if not customChecks then return end 103 | local checkZone = checkZone(GetEntityCoords(cache.ped)) 104 | if checkZone then 105 | notifySystem({ 106 | title = _Lang('placingBench.inAnotherZone'), 107 | type = 'error', 108 | position = Config.libText.notfiyPoistion, 109 | }) 110 | return 111 | end 112 | placingBench(type) 113 | end 114 | 115 | exports('placeBench', placeBench) -------------------------------------------------------------------------------- /server/inventory/qbInventory.lua: -------------------------------------------------------------------------------- 1 | if Config.inventory ~= 'qb-inventory' then return end 2 | local QBCore = exports['qb-core']:GetCoreObject() 3 | 4 | function removeItem(source, item, amount) 5 | local player = QBCore.Functions.GetPlayer(source) 6 | local result = player.Functions.RemoveItem(item, amount) 7 | if not result then return end 8 | TriggerClientEvent('inventory:client:ItemBox', source, QBCore.Shared.Items[item], 'remove') 9 | return true 10 | -- return exports['qb-inventory']:RemoveItem(source, item, amount) 11 | end 12 | 13 | function checkItem(source, item, amount) 14 | return exports['qb-inventory']:HasItem(source, item, amount) 15 | end 16 | 17 | function getItems(source, item) 18 | return exports['qb-inventory']:GetItemByName(source, item) 19 | end 20 | 21 | function giveItem(source, item, amount) 22 | local player = QBCore.Functions.GetPlayer(source) 23 | player.Functions.AddItem(item, amount) 24 | TriggerClientEvent('inventory:client:ItemBox', source, QBCore.Shared.Items[item], 'add') 25 | end 26 | 27 | function createItem(name, trigger, data) 28 | QBCore.Functions.CreateUseableItem(name, function(source, item) 29 | TriggerClientEvent(trigger, source, data) 30 | end) 31 | end 32 | 33 | -- THESE ARE EXAMPLES OF BLUEPRINTS / FOLLOW THE DOCS FOR MORE INFORMATION 34 | 35 | -- -- Crafting 36 | -- weap_bench = {name = 'weap_bench', label = 'Weapons Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Weapons Bench'}, 37 | -- misc_bench = {name = 'misc_bench', label = 'Misc Bench', weight = 100, type = 'item', image = 'pure_bench.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'A Misc Bench'}, 38 | 39 | -- -- Blueprints 40 | -- blueprint_molotov = {name = 'blueprint_molotov', label = 'Molotov Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Molotov Blueprint'}, 41 | -- blueprint_grip = {name = 'blueprint_grip', label = 'Grip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Grip Blueprint'}, 42 | -- blueprint_suppressor = {name = 'blueprint_suppressor', label = 'Suppressor Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Suppressor Blueprint'}, 43 | -- blueprint_extendedclip = {name = 'blueprint_extendedclip', label = 'Extended Clip Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Extended Clip Blueprint'}, 44 | -- blueprint_scope = {name = 'blueprint_scope', label = 'Scope Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Scope Blueprint'}, 45 | -- blueprint_specialcarbine = {name = 'blueprint_specialcarbine', label = 'Special Carbine Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Special Carbine Blueprint'}, 46 | -- blueprint_assaultrifle = {name = 'blueprint_assaultrifle', label = 'Assault Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Assault Rifle Blueprint'}, 47 | -- blueprint_advancedrifle = {name = 'blueprint_advancedrifle', label = 'Advanced Rifle Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Advanced Rifle Blueprint'}, 48 | -- blueprint_sawnoffshotgun = {name = 'blueprint_sawnoffshotgun', label = 'Sawn Off Shotgun Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Sawn Off Shotgun Blueprint'}, 49 | -- blueprint_machinepistol = {name = 'blueprint_machinepistol', label = 'Machine Pistol Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Machien Pistol Blueprint'}, 50 | -- blueprint_microsmg = {name = 'blueprint_microsmg', label = 'Microsmg Blueprint', weight = 100, type = 'item', image = 'pure_blueprint.png', unique = false, useable = true, shouldClose = true, combinable = nil, description = 'Microsmg Blueprint'}, 51 | -------------------------------------------------------------------------------- /server/inventory/oxInventory.lua: -------------------------------------------------------------------------------- 1 | if Config.inventory ~= 'ox_inventory' then return end 2 | 3 | function removeItem(source, item, amount) 4 | return exports.ox_inventory:RemoveItem(source, item, amount) 5 | end 6 | 7 | function checkItem(source, item, amount) 8 | local oxAmount = exports.ox_inventory:Search(source, 'count', item) 9 | if oxAmount >= amount then return true end 10 | return 11 | end 12 | 13 | function getItems(source, item) 14 | local amt = exports.ox_inventory:Search(source, 'count', item) 15 | if not amt then return end 16 | local myAmount = { 17 | amount = amt 18 | } 19 | return myAmount 20 | end 21 | 22 | function giveItem(source, item, amount) 23 | local canCarry = exports.ox_inventory:CanCarryItem(source, item, amount) 24 | if not canCarry then return end 25 | local success, response = exports.ox_inventory:AddItem(source, item, amount) 26 | return success 27 | end 28 | 29 | function createItem(name, trigger, data) 30 | if Config.framework == 'esx' then 31 | ESX.RegisterUsableItem(name, function(source) 32 | TriggerClientEvent(trigger, source, data) 33 | end) 34 | return 35 | end 36 | local QBCore = exports['qb-core']:GetCoreObject() 37 | QBCore.Functions.CreateUseableItem(name, function(source, item) 38 | TriggerClientEvent(trigger, source, data) 39 | end) 40 | end 41 | 42 | -- THESE ARE EXAMPLES OF BLUEPRINTS / FOLLOW THE DOCS FOR MORE INFORMATION 43 | -- -- Crafting: 44 | -- ["weap_bench"] = { 45 | -- label = "Weapons Bench", 46 | -- weight = 4000, 47 | -- stack = true, 48 | -- close = true, 49 | -- description = "A bench to craft weapons", 50 | -- client = { 51 | -- image = "pure_bench.png", 52 | -- } 53 | -- }, 54 | 55 | -- ["misc_bench"] = { 56 | -- label = "Misc Bench", 57 | -- weight = 4000, 58 | -- stack = true, 59 | -- close = true, 60 | -- description = "A bench to craft miscellaneous items", 61 | -- client = { 62 | -- image = "pure_bench.png", 63 | -- } 64 | -- }, 65 | 66 | -- -- Blueprints: 67 | -- ["blueprint_molotov"] = { 68 | -- label = "Molotov Blueprint", 69 | -- weight = 4000, 70 | -- stack = true, 71 | -- close = true, 72 | -- description = "A Blueprint to craft Molotovs", 73 | -- client = { 74 | -- image = "pure_blueprint.png", 75 | -- } 76 | -- }, 77 | 78 | 79 | -- ["blueprint_grip"] = { 80 | -- label = "Grip Blueprint", 81 | -- weight = 4000, 82 | -- stack = true, 83 | -- close = true, 84 | -- description = "A Blueprint to craft a Weapons Grip", 85 | -- client = { 86 | -- image = "pure_blueprint.png", 87 | -- } 88 | -- }, 89 | 90 | -- ["blueprint_suppressor"] = { 91 | -- label = "Suppressor Blueprint", 92 | -- weight = 4000, 93 | -- stack = true, 94 | -- close = true, 95 | -- description = "A Blueprint to craft Suppressors", 96 | -- client = { 97 | -- image = "pure_blueprint.png", 98 | -- } 99 | -- }, 100 | 101 | -- ["blueprint_extendedclip"] = { 102 | -- label = "Extended Clip Blueprint", 103 | -- weight = 4000, 104 | -- stack = true, 105 | -- close = true, 106 | -- description = "A Blueprint to craft Extended Clips", 107 | -- client = { 108 | -- image = "pure_blueprint.png", 109 | -- } 110 | -- }, 111 | 112 | -- ["blueprint_scope"] = { 113 | -- label = "Scope Blueprint", 114 | -- weight = 4000, 115 | -- stack = true, 116 | -- close = true, 117 | -- description = "A Blueprint to craft Scopes", 118 | -- client = { 119 | -- image = "pure_blueprint.png", 120 | -- } 121 | -- }, 122 | 123 | -- ["blueprint_specialcarbine"] = { 124 | -- label = "Special Carbine Blueprint", 125 | -- weight = 4000, 126 | -- stack = true, 127 | -- close = true, 128 | -- description = "A Blueprint to craft a Special Carbine", 129 | -- client = { 130 | -- image = "pure_blueprint.png", 131 | -- } 132 | -- }, 133 | 134 | -- ["blueprint_assaultrifle"] = { 135 | -- label = "Assault Rifle Blueprint", 136 | -- weight = 4000, 137 | -- stack = true, 138 | -- close = true, 139 | -- description = "A Blueprint to craft a Assault Rifle", 140 | -- client = { 141 | -- image = "pure_blueprint.png", 142 | -- } 143 | -- }, 144 | 145 | -- ["blueprint_advancedrifle"] = { 146 | -- label = "Advanced Rifle Blueprint", 147 | -- weight = 4000, 148 | -- stack = true, 149 | -- close = true, 150 | -- description = "A Blueprint to craft a Advanced Rifle", 151 | -- client = { 152 | -- image = "pure_blueprint.png", 153 | -- } 154 | -- }, 155 | 156 | -- ["blueprint_sawnoffshotgun"] = { 157 | -- label = "Sawn Off Shotgun Blueprint", 158 | -- weight = 4000, 159 | -- stack = true, 160 | -- close = true, 161 | -- description = "A Blueprint to craft a Sawn Off Shotgun", 162 | -- client = { 163 | -- image = "pure_blueprint.png", 164 | -- } 165 | -- }, 166 | 167 | -- ["blueprint_machinepistol"] = { 168 | -- label = "Machine Pistol Blueprint", 169 | -- weight = 4000, 170 | -- stack = true, 171 | -- close = true, 172 | -- description = "A Blueprint to craft a Machine Pistol", 173 | -- client = { 174 | -- image = "pure_blueprint.png", 175 | -- } 176 | -- }, 177 | 178 | -- ["blueprint_microsmg"] = { 179 | -- label = "Micro SMG Blueprint", 180 | -- weight = 4000, 181 | -- stack = true, 182 | -- close = true, 183 | -- description = "A Blueprint to craft a Micro SMG", 184 | -- client = { 185 | -- image = "pure_blueprint.png", 186 | -- } 187 | -- }, 188 | -------------------------------------------------------------------------------- /web/src/components/Crafting/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from 'react'; 2 | import { useAppSelector } from '../../store/store'; 3 | import Button from './Button'; 4 | import style from './index.module.css'; 5 | import { craftingItem } from '../../types/crafting'; 6 | import Item from './Item'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons'; 9 | import Blueprint from './Blueprint'; 10 | import { checkFave } from '../../utils/checkFave'; 11 | 12 | const Crafting = () => { 13 | const theme = useAppSelector((state) => state.config.theme); 14 | const crafting = useAppSelector((state) => state.crafting); 15 | const blueprints = crafting.blueprints; 16 | const categories = useAppSelector((state) => state.ui.categories); 17 | const faves = useAppSelector((state) => state.ui.faves); 18 | const [craftingItems, setCraftingItems] = useState([]); 19 | const [search, setSearch] = useState(''); 20 | const scrollRef = useRef(null); 21 | const [displayBlueprints, setDisplayBlueprints] = useState(false); 22 | 23 | const buttonClick = (category: string) => { 24 | sortItems(category); 25 | }; 26 | 27 | const handleWheel = (evt: any) => { 28 | const delta = evt.deltaY; 29 | if (scrollRef.current) { 30 | scrollRef.current.scrollLeft += delta; 31 | } 32 | }; 33 | 34 | const sortItems = (category: string) => { 35 | let array: craftingItem[] = []; 36 | if (category === 'all') { 37 | array = crafting.items; 38 | setCraftingItems(array); 39 | setDisplayBlueprints(true); 40 | return; 41 | } 42 | 43 | crafting.items.map((item: craftingItem) => { 44 | if (category === 'fave' && !checkFave(item.itemName, faves)) { 45 | return; 46 | } 47 | 48 | if (item.category === category) { 49 | array.push(item); 50 | } 51 | }); 52 | 53 | if (category === 'fave') { 54 | setDisplayBlueprints(false); 55 | } else { 56 | setDisplayBlueprints(true); 57 | } 58 | 59 | setCraftingItems(array); 60 | }; 61 | 62 | useEffect(() => { 63 | sortItems('all'); 64 | }, [crafting.items]); 65 | 66 | useMemo(() => { 67 | sortItems('all'); 68 | }, []); 69 | 70 | return ( 71 |
72 |
73 |
93 |
99 | 107 | { 114 | setSearch(e.target.value.toLowerCase()); 115 | }} 116 | placeholder='Search...' 117 | /> 118 |
119 |
120 |
121 | {craftingItems.length > 0 && 122 | craftingItems 123 | .filter((item: craftingItem) => { 124 | return search === '' 125 | ? item 126 | : item.name.toLowerCase().includes(search); 127 | }) 128 | .map((item: craftingItem, index: number) => { 129 | return ( 130 | 143 | ); 144 | })} 145 | {blueprints.length > 0 && 146 | displayBlueprints && 147 | blueprints 148 | .filter((item: craftingItem) => { 149 | return search === '' 150 | ? item 151 | : item.name.toLowerCase().includes(search); 152 | }) 153 | .map((item: craftingItem, index: number) => { 154 | return ( 155 | 168 | ); 169 | })} 170 |
171 | 172 | ); 173 | }; 174 | 175 | export default Crafting; 176 | -------------------------------------------------------------------------------- /web/src/store/stores/crafting/crafting.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { craftingState } from '../../../types/crafting'; 3 | 4 | const initialState: craftingState = { 5 | items: [ 6 | { 7 | itemName: 'assualtrifle', 8 | name: 'Assault Rifle', 9 | image: 10 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 11 | category: 'fave', 12 | id: 0, 13 | description: 'A fuckin cool ar', 14 | craftingTime: 10, 15 | requiredItems: [ 16 | { 17 | itemName: 'wood', 18 | name: 'Wood', 19 | amount: 2, 20 | myAmount: 4, 21 | image: 22 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 23 | }, 24 | { 25 | itemName: 'metal', 26 | name: 'Metal', 27 | amount: 2, 28 | myAmount: 1, 29 | image: 30 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 31 | }, 32 | ], 33 | }, 34 | { 35 | itemName: 'assualtrifle', 36 | name: 'Assault Rifle2', 37 | image: 38 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 39 | category: 'fave', 40 | id: 3, 41 | description: 'A fuckin cool ar', 42 | craftingTime: 10, 43 | requiredItems: [ 44 | { 45 | itemName: 'wood', 46 | name: 'Wood', 47 | amount: 2, 48 | myAmount: 4, 49 | image: 50 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 51 | }, 52 | { 53 | itemName: 'metal', 54 | name: 'Metal', 55 | amount: 2, 56 | myAmount: 1, 57 | image: 58 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 59 | }, 60 | ], 61 | }, 62 | { 63 | itemName: 'lockpick', 64 | name: 'Lockpick', 65 | type: 'blueprint', 66 | image: 67 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 68 | category: 'blueprints', 69 | id: 9999, 70 | description: 'Blueprint for a lockpick', 71 | craftingTime: 10, 72 | uses: 1, 73 | requiredItems: [ 74 | { 75 | itemName: 'wood', 76 | name: 'Wood', 77 | amount: 2, 78 | myAmount: 4, 79 | image: 80 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 81 | }, 82 | { 83 | itemName: 'metal', 84 | name: 'Metal', 85 | amount: 2, 86 | myAmount: 1, 87 | image: 88 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 89 | }, 90 | ], 91 | }, 92 | { 93 | itemName: 'lockpick', 94 | name: 'Lockpick', 95 | type: 'blueprint', 96 | image: 97 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 98 | category: 'blueprints', 99 | id: 9999, 100 | description: 'Blueprint for a lockpick', 101 | craftingTime: 10, 102 | uses: 1, 103 | requiredItems: [ 104 | { 105 | itemName: 'wood', 106 | name: 'Wood', 107 | amount: 2, 108 | myAmount: 4, 109 | image: 110 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 111 | }, 112 | { 113 | itemName: 'metal', 114 | name: 'Metal', 115 | amount: 2, 116 | myAmount: 1, 117 | image: 118 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 119 | }, 120 | ], 121 | }, 122 | { 123 | itemName: 'lockpick', 124 | name: 'Lockpick', 125 | type: 'blueprint', 126 | image: 127 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 128 | category: 'blueprints', 129 | id: 9999, 130 | description: 'Blueprint for a lockpick', 131 | craftingTime: 10, 132 | uses: 1, 133 | requiredItems: [ 134 | { 135 | itemName: 'wood', 136 | name: 'Wood', 137 | amount: 2, 138 | myAmount: 4, 139 | image: 140 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 141 | }, 142 | { 143 | itemName: 'metal', 144 | name: 'Metal', 145 | amount: 2, 146 | myAmount: 1, 147 | image: 148 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084690112544851/duffelbag.png?ex=663dffb8&is=663cae38&hm=45e7bb2338c6f18a784c5e154cbb35f89676f59431b86648ec5471095563a8cc&', 149 | }, 150 | ], 151 | }, 152 | ], 153 | blueprints: [ 154 | { 155 | itemName: 'assualtrifle', 156 | name: 'Assault Rifle Blueprint', 157 | image: 158 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084689542123620/boogieboard.png?ex=663dffb8&is=663cae38&hm=fec9895177f964704e67814fd948d9ed336a62f6f3f1366ff326c7741544bd88&', 159 | category: 'blueprints', 160 | id: 35, 161 | description: 'Assault Rifle Blueprint', 162 | craftingTime: 10, 163 | uses: 1, 164 | requiredItems: [ 165 | { 166 | itemName: 'wood', 167 | name: 'Wood', 168 | amount: 2, 169 | myAmount: 4, 170 | image: 171 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084689542123620/boogieboard.png?ex=663dffb8&is=663cae38&hm=fec9895177f964704e67814fd948d9ed336a62f6f3f1366ff326c7741544bd88&', 172 | }, 173 | { 174 | itemName: 'metal', 175 | name: 'Metal', 176 | amount: 2, 177 | myAmount: 1, 178 | image: 179 | 'https://cdn.discordapp.com/attachments/789185814768386088/1238084689542123620/boogieboard.png?ex=663dffb8&is=663cae38&hm=fec9895177f964704e67814fd948d9ed336a62f6f3f1366ff326c7741544bd88&', 180 | }, 181 | ], 182 | }, 183 | ], 184 | selectedItem: 0, 185 | currentItem: null, 186 | amount: 1, 187 | }; 188 | 189 | export const craftingSlice = createSlice({ 190 | name: 'crafting', 191 | initialState, 192 | reducers: { 193 | setItems: (state, action: PayloadAction) => { 194 | state.items = action.payload.items; 195 | // state.currentItem = null; 196 | // state.selectedItem = 0; 197 | if (!state.currentItem) return; 198 | if (state.currentItem.type !== 'item' && !action.payload.confg) { 199 | state.currentItem = null; 200 | state.selectedItem = 0; 201 | } else { 202 | for (let i = 0; i < state.items.length; i++) { 203 | if (state.currentItem && state.items[i].id == state.currentItem.id) { 204 | state.currentItem.requiredItems = state.items[i].requiredItems; 205 | } 206 | } 207 | } 208 | }, 209 | setBlueprints: (state, action: PayloadAction) => { 210 | state.blueprints = action.payload; 211 | }, 212 | setSelected: ( 213 | state, 214 | action: PayloadAction<{ 215 | id: number; 216 | type: string; 217 | }> 218 | ) => { 219 | if ( 220 | action.payload.type === 'item' || 221 | action.payload.type === 'blueprint' 222 | ) { 223 | if (state.items.length === 0) return; 224 | state.selectedItem = action.payload.id; 225 | for (let i = 0; i < state.items.length; i++) { 226 | if (state.items[i].id === state.selectedItem) { 227 | state.currentItem = state.items[i]; 228 | state.currentItem.type = action.payload.type; 229 | return; 230 | } 231 | } 232 | return; 233 | } else { 234 | if (state.blueprints.length === 0) return; 235 | state.selectedItem = action.payload.id; 236 | for (let i = 0; i < state.blueprints.length; i++) { 237 | if (state.blueprints[i].id === state.selectedItem) { 238 | state.currentItem = state.blueprints[i]; 239 | state.currentItem.type = action.payload.type; 240 | return; 241 | } 242 | } 243 | } 244 | }, 245 | setAmount: (state, action: PayloadAction) => { 246 | state.amount = action.payload; 247 | }, 248 | }, 249 | }); 250 | 251 | export default craftingSlice.reducer; 252 | export const { setItems, setBlueprints, setSelected, setAmount } = 253 | craftingSlice.actions; 254 | -------------------------------------------------------------------------------- /web/src/components/Info/index.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import style from './index.module.css'; 3 | import { faClock, faHammer, faStar } from '@fortawesome/free-solid-svg-icons'; 4 | import { useAppSelector } from '../../store/store'; 5 | import Required from './Required'; 6 | import Queue from './Queue'; 7 | import Bottom from './Bottom'; 8 | import { useEffect } from 'react'; 9 | import updateRipples from '../../utils/updateRipples'; 10 | import { sendNui } from '../../utils/sendNui'; 11 | 12 | const Info = () => { 13 | const theme = useAppSelector((state) => state.config.theme); 14 | const language = useAppSelector((state) => state.config.language); 15 | const config = useAppSelector((state) => state.config.config); 16 | // const items = useAppSelector((state) => state.crafting); 17 | const item = useAppSelector((state) => state.crafting.currentItem); 18 | const queue = useAppSelector((state) => state.queue); 19 | 20 | useEffect(() => { 21 | updateRipples(); 22 | }); 23 | 24 | if (item && item.type === 'blaze') { 25 | return ( 26 |
32 |
33 |
41 | 42 |
43 |
49 | {item && } 50 |
51 |
56 |

57 | {item ? item.name : language.noItemSelected} 58 |

59 |

60 | {item ? item.description : language.noItemSelected} 61 |

62 |
63 | 70 |

75 | {language.craftTime}{' '} 76 | 80 | {item ? item.craftingTime : 0} 81 | {language.s} 82 | 83 |

84 |
85 |
86 | 93 |

98 | {language.uses}{' '} 99 | 103 | 1 104 | 105 |

106 |
107 |
108 |
109 |
110 |
111 | 112 |
113 |
114 |
120 |
{/* */}
121 |
122 | ); 123 | } 124 | 125 | return ( 126 |
132 |
133 | {config.enableFavourites && ( 134 |
{ 138 | if (!item || item?.type !== 'item') { 139 | return; 140 | } 141 | sendNui('setFavourite', { 142 | itemName: item.itemName, 143 | }); 144 | }} 145 | style={{ 146 | background: theme.button, 147 | border: `0.2vw solid ${theme.border}`, 148 | color: theme.white, 149 | }}> 150 | 151 |
152 | )} 153 | {/* {props.type === 'blueprint' && config.autoMakeBlueprint ? ( 154 |
159 | 160 |
161 | ) : ( 162 | 163 | )} */} 164 |
170 | {item && } 171 |
172 |
177 |

178 | {item ? item.name : language.noItemSelected} 179 |

180 |

181 | {item ? item.description : language.noItemSelected} 182 |

183 |
184 | 191 |

196 | {language.craftTime}{' '} 197 | 201 | {item ? item.craftingTime : 0} 202 | {language.s} 203 | 204 |

205 |
206 | {item && 207 | item.uses && 208 | item.uses > 1 && 209 | !config.unlimitedBlueprints && ( 210 |
211 | 218 |

223 | {language.uses}{' '} 224 | 228 | {item ? (item.uses ? item.uses : 0) : 0} 229 | 230 |

231 |
232 | )} 233 |
234 |
235 |
236 |
237 |

242 | {language.required} 243 |

244 | 245 |
246 |
247 |
253 | {queue.finished.length > 0 && 254 | queue.finished.map((queueItem, index) => { 255 | return ( 256 | 266 | ); 267 | })} 268 | {queue.items.length > 0 && 269 | queue.items.map((queueItem, index) => { 270 | const startTimer = index === 0; 271 | return ( 272 | 282 | ); 283 | })} 284 |
285 |
286 | 287 |
288 |
289 | ); 290 | }; 291 | 292 | export default Info; 293 | -------------------------------------------------------------------------------- /config/item_blueprints.lua: -------------------------------------------------------------------------------- 1 | -- the blueprint will have an id attached and will then index the table to provide the proper information such as required items, etc etc 2 | -- These ids cannot conflict with the crafting ids hence the big number 3 | ImageDirectory = 'https://cfx-nui-qb-inventory/html/images/' 4 | 5 | if Config.inventory == 'ox_inventory' then 6 | ImageDirectory = 'nui://ox_inventory/web/images/' 7 | elseif Config.inventory == 'ps-inventory' then 8 | ImageDirectory = 'nui://ps-inventory/html/images/' 9 | end 10 | 11 | Config.blueprints = { 12 | ids = { 13 | weapon = { 14 | { 15 | blueprintId = 'blueprint_microsmg', 16 | id = 99991, 17 | itemName = 'weapon_microsmg', 18 | name = 'Micro SMG', 19 | image = ImageDirectory..'weapon_microsmg.png', 20 | category = 'blueprints', -- DONT TOUCH THIS!!! 21 | type = 'blueprint', -- DONT TOUCH THIS!!! 22 | description = 'Blueprints for a Micro SMG', 23 | craftingTime = 20, 24 | requiredItems = { 25 | { 26 | itemName = 'metalscrap', 27 | name = 'Metal Scrap', 28 | amount = 20, 29 | myAmount = 0, 30 | image = ImageDirectory..'metalscrap.png', 31 | }, 32 | }, 33 | }, 34 | { 35 | blueprintId = 'blueprint_machinepistol', 36 | id = 99992, 37 | itemName = 'weapon_machinepistol', 38 | name = 'Machine Pistol', 39 | image = ImageDirectory..'weapon_machinepistol.png', 40 | category = 'blueprints', -- DONT TOUCH THIS!!! 41 | type = 'blueprint', -- DONT TOUCH THIS!!! 42 | description = 'Blueprints for a Machine Pistol', 43 | craftingTime = 20, 44 | requiredItems = { 45 | { 46 | itemName = 'metalscrap', 47 | name = 'Metal Scrap', 48 | amount = 20, 49 | myAmount = 0, 50 | image = ImageDirectory..'metalscrap.png', 51 | }, 52 | }, 53 | }, 54 | { 55 | blueprintId = 'blueprint_sawnoffshotgun', 56 | id = 99993, 57 | itemName = 'weapon_sawnoffshotgun', 58 | name = 'Sawn Off Shotgun', 59 | image = ImageDirectory..'weapon_sawnoffshotgun.png', 60 | category = 'blueprints', -- DONT TOUCH THIS!!! 61 | type = 'blueprint', -- DONT TOUCH THIS!!! 62 | description = 'Blueprints for a Shawn Off Shotgun', 63 | craftingTime = 20, 64 | requiredItems = { 65 | { 66 | itemName = 'metalscrap', 67 | name = 'Metal Scrap', 68 | amount = 20, 69 | myAmount = 0, 70 | image = ImageDirectory..'metalscrap.png', 71 | }, 72 | }, 73 | }, 74 | { 75 | blueprintId = 'blueprint_advancedrifle', 76 | id = 99994, 77 | itemName = 'weapon_advancedrifle', 78 | name = 'Advanced Rifle', 79 | image = ImageDirectory..'weapon_advancedrifle.png', 80 | category = 'blueprints', -- DONT TOUCH THIS!!! 81 | type = 'blueprint', -- DONT TOUCH THIS!!! 82 | description = 'Blueprints for a Advanced Rifle', 83 | craftingTime = 20, 84 | requiredItems = { 85 | { 86 | itemName = 'metalscrap', 87 | name = 'Metal Scrap', 88 | amount = 20, 89 | myAmount = 0, 90 | image = ImageDirectory..'metalscrap.png', 91 | }, 92 | }, 93 | }, 94 | { 95 | blueprintId = 'blueprint_assaultrifle', 96 | id = 99995, 97 | itemName = 'weapon_assaultrifle', 98 | name = 'Assault Rifle', 99 | image = ImageDirectory..'weapon_assaultrifle.png', 100 | category = 'blueprints', -- DONT TOUCH THIS!!! 101 | type = 'blueprint', -- DONT TOUCH THIS!!! 102 | description = 'Blueprints for a Assault Rifle', 103 | craftingTime = 20, 104 | requiredItems = { 105 | { 106 | itemName = 'metalscrap', 107 | name = 'Metal Scrap', 108 | amount = 20, 109 | myAmount = 0, 110 | image = ImageDirectory..'metalscrap.png', 111 | }, 112 | }, 113 | }, 114 | { 115 | blueprintId = 'blueprint_speacialcarbine', 116 | id = 99996, 117 | itemName = 'weapon_specialcarbine', 118 | name = 'Special Carbine', 119 | image = ImageDirectory..'weapon_specialcarbine.png', 120 | category = 'blueprints', -- DONT TOUCH THIS!!! 121 | type = 'blueprint', -- DONT TOUCH THIS!!! 122 | description = 'Blueprints for a Special Carbine', 123 | craftingTime = 20, 124 | requiredItems = { 125 | { 126 | itemName = 'metalscrap', 127 | name = 'Metal Scrap', 128 | amount = 20, 129 | myAmount = 0, 130 | image = ImageDirectory..'metalscrap.png', 131 | }, 132 | }, 133 | }, 134 | }, 135 | misc = { 136 | { 137 | blueprintId = 'blueprint_scope', 138 | id = 99991, 139 | itemName = 'holoscope_attachment', 140 | name = 'Weapon Holoscope', 141 | image = ImageDirectory..'holoscope_attachment.png', 142 | category = 'blueprints', -- DONT TOUCH THIS!!! 143 | type = 'blueprint', -- DONT TOUCH THIS!!! 144 | description = 'Blueprints for a Holographic Scope', 145 | craftingTime = 20, 146 | requiredItems = { 147 | { 148 | itemName = 'metalscrap', 149 | name = 'Metal Scrap', 150 | amount = 20, 151 | myAmount = 0, 152 | image = ImageDirectory..'metalscrap.png', 153 | }, 154 | }, 155 | }, 156 | { 157 | blueprintId = 'blueprint_extendedclip', 158 | id = 99992, 159 | itemName = 'clip_attachment', 160 | name = 'Extended Clip', 161 | image = ImageDirectory..'clip_attachment.png', 162 | category = 'blueprints', -- DONT TOUCH THIS!!! 163 | type = 'blueprint', -- DONT TOUCH THIS!!! 164 | description = 'Blueprints for a Extended Clip', 165 | craftingTime = 20, 166 | requiredItems = { 167 | { 168 | itemName = 'metalscrap', 169 | name = 'Metal Scrap', 170 | amount = 20, 171 | myAmount = 0, 172 | image = ImageDirectory..'metalscrap.png', 173 | }, 174 | }, 175 | }, 176 | { 177 | blueprintId = 'blueprint_suppressor', 178 | id = 99993, 179 | itemName = 'suppressor_attachment', 180 | name = 'Weapon Suppressor', 181 | image = ImageDirectory..'suppressor_attachment.png', 182 | category = 'blueprints', -- DONT TOUCH THIS!!! 183 | type = 'blueprint', -- DONT TOUCH THIS!!! 184 | description = 'Blueprints for a Suppressorr', 185 | craftingTime = 20, 186 | requiredItems = { 187 | { 188 | itemName = 'metalscrap', 189 | name = 'Metal Scrap', 190 | amount = 20, 191 | myAmount = 0, 192 | image = ImageDirectory..'metalscrap.png', 193 | }, 194 | }, 195 | }, 196 | { 197 | blueprintId = 'blueprint_grip', 198 | id = 99994, 199 | itemName = 'grip_attachment', 200 | name = 'Weapon Grip', 201 | image = ImageDirectory..'grip_attachment.png', 202 | category = 'blueprints', -- DONT TOUCH THIS!!! 203 | type = 'blueprint', -- DONT TOUCH THIS!!! 204 | description = 'Blueprints for a Weapon Grip', 205 | craftingTime = 20, 206 | requiredItems = { 207 | { 208 | itemName = 'metalscrap', 209 | name = 'Metal Scrap', 210 | amount = 20, 211 | myAmount = 0, 212 | image = ImageDirectory..'metalscrap.png', 213 | }, 214 | }, 215 | }, 216 | { 217 | blueprintId = 'blueprint_molotov', 218 | id = 99995, 219 | itemName = 'weapon_molotov', 220 | name = 'Molotov', 221 | image = ImageDirectory..'weapon_molotov.png', 222 | category = 'blueprints', -- DONT TOUCH THIS!!! 223 | type = 'blueprint', -- DONT TOUCH THIS!!! 224 | description = 'Blueprints for a Molotov', 225 | craftingTime = 20, 226 | requiredItems = { 227 | { 228 | itemName = 'metalscrap', 229 | name = 'Metal Scrap', 230 | amount = 20, 231 | myAmount = 0, 232 | image = ImageDirectory..'metalscrap.png', 233 | }, 234 | }, 235 | }, 236 | }, 237 | }, 238 | 239 | -- These are all the useable items in which it create a useable item 240 | items = { 241 | 'blueprint_molotov', 242 | 'blueprint_grip', 243 | 'blueprint_suppressor', 244 | 'blueprint_extendedclip', 245 | 'blueprint_scope', 246 | 'blueprint_specialcarbine', 247 | 'blueprint_assaultrifle', 248 | 'blueprint_advancedrifle', 249 | 'blueprint_sawnoffshotgun', 250 | 'blueprint_machinepistol', 251 | 'blueprint_microsmg', 252 | }, 253 | } 254 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, Suspense, lazy } from 'react'; 2 | import { getThemeVariables } from './utils/getThemeVariables'; 3 | import { useAppDistpatch, useAppSelector } from './store/store'; 4 | import { fetchNui } from './utils/fetchNui'; 5 | import { setConfig, setLanguage, setTheme } from './store/stores/config/config'; 6 | import { debugData } from './utils/debugData'; 7 | import style from './index.module.css'; 8 | import Crafting from './components/Crafting'; 9 | import Info from './components/Info'; 10 | import { useNuiEvent } from './hooks/useNuiEvent'; 11 | import { 12 | addItem, 13 | setQueueFinishedItems, 14 | setQueueItems, 15 | updateSecondsChange, 16 | } from './store/stores/crafting/queue'; 17 | import { setBlueprints, setItems } from './store/stores/crafting/crafting'; 18 | import { setCategories, setFaves } from './store/stores/crafting/ui'; 19 | import updateRipples from './utils/updateRipples'; 20 | const Popup = lazy(() => import('./components/Popup')); 21 | 22 | debugData([ 23 | { 24 | action: 'setVisible', 25 | data: true, 26 | }, 27 | ]); 28 | 29 | const App = () => { 30 | const dispatch = useAppDistpatch(); 31 | const [loaded, setLoaded] = useState(false); 32 | const [configLoaded, setConfigLoaded] = useState(false); 33 | const popup = useAppSelector((state) => state.popup); 34 | const config = useAppSelector((state) => state.config.config); 35 | 36 | useMemo(() => { 37 | const asyncConfig = async () => { 38 | const themeVariables = await getThemeVariables(); 39 | dispatch(setTheme(themeVariables)); 40 | setLoaded(true); 41 | updateRipples(); 42 | }; 43 | asyncConfig(); 44 | 45 | fetchNui( 46 | 'getConfig', 47 | {}, 48 | { 49 | items: [ 50 | { 51 | itemName: 'assualtrifle', 52 | name: 'Assault Rifle', 53 | image: 54 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 55 | category: 'fave', 56 | id: 0, 57 | description: 'A fuckin cool ar', 58 | craftingTime: 10, 59 | requiredItems: [ 60 | { 61 | itemName: 'wood', 62 | name: 'Wood', 63 | amount: 2, 64 | myAmount: 4, 65 | image: 66 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 67 | }, 68 | { 69 | itemName: 'metal', 70 | name: 'Metal', 71 | amount: 2, 72 | myAmount: 1, 73 | image: 74 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 75 | }, 76 | ], 77 | }, 78 | { 79 | itemName: 'assualtrifle', 80 | name: 'Assault Rifle2', 81 | image: 82 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 83 | category: 'fave', 84 | id: 3, 85 | description: 'A fuckin cool ar', 86 | craftingTime: 10, 87 | requiredItems: [ 88 | { 89 | itemName: 'wood', 90 | name: 'Wood', 91 | amount: 2, 92 | myAmount: 4, 93 | image: 94 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 95 | }, 96 | { 97 | itemName: 'metal', 98 | name: 'Metal', 99 | amount: 2, 100 | myAmount: 1, 101 | image: 102 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 103 | }, 104 | ], 105 | }, 106 | { 107 | itemName: 'lockpick', 108 | name: 'Lockpick', 109 | type: 'blueprint', 110 | image: 111 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 112 | category: 'blueprints', 113 | id: 9999, 114 | description: 'Blueprint for a lockpick', 115 | craftingTime: 10, 116 | uses: 1, 117 | requiredItems: [ 118 | { 119 | itemName: 'wood', 120 | name: 'Wood', 121 | amount: 2, 122 | myAmount: 4, 123 | image: 124 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 125 | }, 126 | { 127 | itemName: 'metal', 128 | name: 'Metal', 129 | amount: 2, 130 | myAmount: 1, 131 | image: 132 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 133 | }, 134 | ], 135 | }, 136 | { 137 | itemName: 'assualtrifle', 138 | name: 'Assault Rifle2', 139 | image: 140 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 141 | category: 'fave', 142 | id: 3, 143 | description: 'A fuckin cool ar', 144 | craftingTime: 10, 145 | requiredItems: [ 146 | { 147 | itemName: 'wood', 148 | name: 'Wood', 149 | amount: 2, 150 | myAmount: 4, 151 | image: 152 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 153 | }, 154 | { 155 | itemName: 'metal', 156 | name: 'Metal', 157 | amount: 2, 158 | myAmount: 1, 159 | image: 160 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 161 | }, 162 | ], 163 | }, 164 | { 165 | itemName: 'lockpick', 166 | name: 'Lockpick', 167 | type: 'blueprint', 168 | image: 169 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 170 | category: 'blueprints', 171 | id: 9999, 172 | description: 'Blueprint for a lockpick', 173 | craftingTime: 10, 174 | uses: 1, 175 | requiredItems: [ 176 | { 177 | itemName: 'wood', 178 | name: 'Wood', 179 | amount: 2, 180 | myAmount: 4, 181 | image: 182 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 183 | }, 184 | { 185 | itemName: 'metal', 186 | name: 'Metal', 187 | amount: 2, 188 | myAmount: 1, 189 | image: 190 | 'https://cdn.discordapp.com/attachments/789185814768386088/1233403703856201739/paintscraper.png?ex=66323e36&is=6630ecb6&hm=2ffdb3677fc28dbf9ebd1c3d402e44350e794f29564b5cb302077f37a797890e&', 191 | }, 192 | ], 193 | }, 194 | ], 195 | categories: [ 196 | { 197 | icon: 'fa-star', 198 | category: 'fave', 199 | }, 200 | { 201 | icon: 'fa-pen-ruler', 202 | category: 'blueprints', 203 | }, 204 | ], 205 | enableFavourites: true, 206 | unlimitedBlueprints: true, 207 | } 208 | ) 209 | .then((config) => { 210 | dispatch(setConfig(config)); 211 | dispatch(setCategories(config.categories)); 212 | // dispatch(setItems(config.items)); 213 | }) 214 | .catch((err) => { 215 | console.log(err); 216 | }); 217 | 218 | fetchNui( 219 | 'getLanguage', 220 | {}, 221 | { 222 | craft: 'Craft', 223 | language: 'Items Required:', 224 | craftTime: 'Crafting Time:', 225 | s: 's', 226 | uses: 'Uses:', 227 | claim: 'CONFIRM', 228 | cancel: 'CANCEL', 229 | yes: 'Yes', 230 | no: 'No', 231 | claimCraft: 'Claim Craft', 232 | cancelCraft: 'Cancel Craft', 233 | areYouSure: 'Are you sure you want to do this?', 234 | noItemSelected: 'No item selected', 235 | required: 'Items Required:', 236 | unlockBP: 'Unlock Blueprint', 237 | } 238 | ) 239 | .then((language) => { 240 | dispatch(setLanguage(language)); 241 | setConfigLoaded(true); 242 | }) 243 | .catch((err) => { 244 | console.log(err); 245 | }); 246 | }, []); 247 | 248 | useNuiEvent('addToQueue', (data) => { 249 | dispatch(addItem(data)); 250 | }); 251 | 252 | useNuiEvent('updateItems', (data) => { 253 | if (data === '[]') { 254 | dispatch(setQueueItems([])); 255 | return; 256 | } 257 | dispatch(setQueueItems(data)); 258 | }); 259 | 260 | useNuiEvent('updateFinished', (data) => { 261 | if (data === '[]') { 262 | dispatch(setQueueFinishedItems([])); 263 | return; 264 | } 265 | dispatch(setQueueFinishedItems(data)); 266 | }); 267 | 268 | useNuiEvent('secondsChange', (data) => { 269 | dispatch(updateSecondsChange(data)); 270 | }); 271 | 272 | useNuiEvent('itemsChange', (data) => { 273 | const test = { 274 | items: data, 275 | confg: config.unlimitedBlueprints, 276 | }; 277 | dispatch(setItems(test)); 278 | }); 279 | 280 | useNuiEvent('blueprints', (data) => { 281 | if (data === '[]') { 282 | dispatch(setBlueprints([])); 283 | return; 284 | } 285 | dispatch(setBlueprints(data)); 286 | }); 287 | 288 | useNuiEvent('setFaves', (data) => { 289 | if (data === '[]') { 290 | dispatch(setFaves([])); 291 | return; 292 | } 293 | dispatch(setFaves(data)); 294 | }); 295 | 296 | if (!loaded) return null; 297 | if (!configLoaded) return null; 298 | 299 | return ( 300 | <> 301 | Loading...}> 302 | {popup.showPopup && } 303 | 304 |
305 | 306 | 307 |
308 | 309 | ); 310 | }; 311 | 312 | export default App; 313 | -------------------------------------------------------------------------------- /config/items.lua: -------------------------------------------------------------------------------- 1 | Config.items = { 2 | weapon = { 3 | { 4 | id = 0, 5 | itemName = 'weapon_heavypistol', 6 | name = 'Heavy Pistol', 7 | image = ImageDirectory..'weapon_heavypistol.png', 8 | description = 'Heavy Pistol', 9 | craftingTime = 10, 10 | requiredItems = { 11 | { 12 | itemName = 'metalscrap', 13 | name = 'Metalscrap', 14 | amount = 50, 15 | myAmount = 0, -- Ignore 16 | image = ImageDirectory..'metalscrap.png', 17 | }, 18 | }, 19 | }, 20 | { 21 | id = 1, 22 | itemName = 'weapon_snspistol', 23 | name = 'SNS Pistol', 24 | image = ImageDirectory..'weapon_snspistol.png', 25 | description = 'SNS Pistol', 26 | craftingTime = 10, 27 | requiredItems = { 28 | { 29 | itemName = 'metalscrap', 30 | name = 'Metal Scrap', 31 | amount = 50, 32 | myAmount = 0, -- Ignore 33 | image = ImageDirectory..'metalscrap.png', 34 | }, 35 | }, 36 | }, 37 | { 38 | id = 2, 39 | itemName = 'weapon_pistol', 40 | name = 'Pistol', 41 | image = ImageDirectory..'weapon_pistol.png', 42 | description = 'Pistol', 43 | craftingTime = 10, 44 | requiredItems = { 45 | { 46 | itemName = 'metalscrap', 47 | name = 'Metal Scrap', 48 | amount = 50, 49 | myAmount = 0, -- Ignore 50 | image = ImageDirectory..'metalscrap.png', 51 | }, 52 | }, 53 | }, 54 | { 55 | id = 3, 56 | itemName = 'weapon_vintagepistol', 57 | name = 'Vintage Pistol', 58 | image = ImageDirectory..'weapon_vintagepistol.png', 59 | description = 'Vintage Pistol', 60 | craftingTime = 10, 61 | requiredItems = { 62 | { 63 | itemName = 'metalscrap', 64 | name = 'Metal Scrap', 65 | amount = 50, 66 | myAmount = 0, -- Ignore 67 | image = ImageDirectory..'metalscrap.png', 68 | }, 69 | }, 70 | }, 71 | { 72 | id = 4, 73 | itemName = 'weapon_pistol50', 74 | name = 'Pistol 50', 75 | image = ImageDirectory..'weapon_pistol50.png', 76 | description = 'Pistol 50', 77 | craftingTime = 10, 78 | requiredItems = { 79 | { 80 | itemName = 'metalscrap', 81 | name = 'Metal Scrap', 82 | amount = 50, 83 | myAmount = 0, -- Ignore 84 | image = ImageDirectory..'metalscrap.png', 85 | }, 86 | }, 87 | }, 88 | }, 89 | misc = { 90 | { 91 | id = 0, 92 | itemName = 'lockpick', 93 | name = 'Lockpick', 94 | image = ImageDirectory..'lockpick.png', 95 | description = 'Lockpick', 96 | craftingTime = 10, 97 | requiredItems = { 98 | { 99 | itemName = 'plastic', 100 | name = 'Plastic', 101 | amount = 10, 102 | myAmount = 0, -- Ignore 103 | image = ImageDirectory..'plastic.png', 104 | }, 105 | }, 106 | }, 107 | { 108 | id = 1, 109 | itemName = 'advancedlockpick', 110 | name = 'Advanced Lockpick', 111 | image = ImageDirectory..'advancedlockpick.png', 112 | description = 'Advanced Lockpick', 113 | craftingTime = 10, 114 | requiredItems = { 115 | { 116 | itemName = 'plastic', 117 | name = 'Plastic', 118 | amount = 10, 119 | myAmount = 0, -- Ignore 120 | image = ImageDirectory..'plastic.png', 121 | }, 122 | { 123 | itemName = 'metalscrap', 124 | name = 'Metal Scrap', 125 | amount = 10, 126 | myAmount = 0, -- Ignore 127 | image = ImageDirectory..'metalscrap.png', 128 | }, 129 | }, 130 | }, 131 | { 132 | id = 2, 133 | itemName = 'repairkit', 134 | name = 'Repair Kit', 135 | image = ImageDirectory..'repairkit.png', 136 | description = 'A Repair Kit', 137 | craftingTime = 10, 138 | requiredItems = { 139 | { 140 | itemName = 'plastic', 141 | name = 'Plastic', 142 | amount = 10, 143 | myAmount = 0, -- Ignore 144 | image = ImageDirectory..'plastic.png', 145 | }, 146 | }, 147 | }, 148 | { 149 | id = 3, 150 | itemName = 'pistol_ammo', 151 | name = 'Pistol Ammo', 152 | image = ImageDirectory..'pistol_ammo.png', 153 | description = 'Pistol Ammo used within Pistols', 154 | craftingTime = 10, 155 | requiredItems = { 156 | { 157 | itemName = 'metalscrap', 158 | name = 'Metal Scrap', 159 | amount = 25, 160 | myAmount = 0, -- Ignore 161 | image = ImageDirectory..'metalscrap.png', 162 | }, 163 | }, 164 | }, 165 | { 166 | id = 4, 167 | itemName = 'smg_ammo', 168 | name = 'SMG Ammo', 169 | image = ImageDirectory..'smg_ammo.png', 170 | description = 'SMG Ammo used within SMGS', 171 | craftingTime = 10, 172 | requiredItems = { 173 | { 174 | itemName = 'metalscrap', 175 | name = 'Metal Scrap', 176 | amount = 35, 177 | myAmount = 0, -- Ignore 178 | image = ImageDirectory..'metalscrap.png', 179 | }, 180 | }, 181 | }, 182 | { 183 | id = 5, 184 | itemName = 'rifle_ammo', 185 | name = 'Rifle Ammo', 186 | image = ImageDirectory..'rifle_ammo.png', 187 | description = 'Rifle Ammo used within Rifles', 188 | craftingTime = 10, 189 | requiredItems = { 190 | { 191 | itemName = 'metalscrap', 192 | name = 'Metal Scrap', 193 | amount = 35, 194 | myAmount = 0, -- Ignore 195 | image = ImageDirectory..'metalscrap.png', 196 | }, 197 | }, 198 | }, 199 | }, 200 | cluckinBell = { 201 | { 202 | id = 0, 203 | itemName = 'pure_chickenbucket', 204 | name = 'Chicken Bucket', 205 | image = ImageDirectory..'pure_chickenbucket.png', 206 | description = 'A Bucket of Chicken', 207 | craftingTime = 30, 208 | requiredItems = { 209 | { 210 | itemName = 'pure_foodingredients', 211 | name = 'Food Ingredients', 212 | amount = 32, 213 | myAmount = 0, -- Ignore 214 | image = ImageDirectory..'pure_foodingredients.png', 215 | }, 216 | }, 217 | }, 218 | { 219 | id = 1, 220 | itemName = 'pure_popcorn', 221 | name = 'Popcorn Chicken', 222 | image = ImageDirectory..'pure_popcorn.png', 223 | description = 'A Bucket of Popcorn Chicken', 224 | craftingTime = 20, 225 | requiredItems = { 226 | { 227 | itemName = 'pure_foodingredients', 228 | name = 'Food Ingredients', 229 | amount = 25, 230 | myAmount = 0, -- Ignore 231 | image = ImageDirectory..'pure_foodingredients.png', 232 | }, 233 | }, 234 | }, 235 | { 236 | id = 2, 237 | itemName = 'pure_drink', 238 | name = 'Coca Cola', 239 | image = ImageDirectory..'pure_drink.png', 240 | description = 'A 500ML Coca Cola', 241 | craftingTime = 10, 242 | requiredItems = { 243 | { 244 | itemName = 'pure_foodingredients', 245 | name = 'Food Ingredients', 246 | amount = 3, 247 | myAmount = 0, -- Ignore 248 | image = ImageDirectory..'pure_foodingredients.png', 249 | }, 250 | }, 251 | }, 252 | { 253 | id = 3, 254 | itemName = 'pure_fries', 255 | name = 'Fries', 256 | image = ImageDirectory..'pure_fries.png', 257 | description = 'Lightly Salted Fries', 258 | craftingTime = 10, 259 | requiredItems = { 260 | { 261 | itemName = 'pure_foodingredients', 262 | name = 'Food Ingredients', 263 | amount = 16, 264 | myAmount = 0, -- Ignore 265 | image = ImageDirectory..'pure_foodingredients.png', 266 | }, 267 | }, 268 | }, 269 | { 270 | id = 4, 271 | itemName = 'pure_onionrings', 272 | name = 'Onion Rings', 273 | image = ImageDirectory..'pure_onionrings.png', 274 | description = 'Onion Rings', 275 | craftingTime = 10, 276 | requiredItems = { 277 | { 278 | itemName = 'pure_foodingredients', 279 | name = 'Food Ingredients', 280 | amount = 8, 281 | myAmount = 0, -- Ignore 282 | image = ImageDirectory..'pure_foodingredients.png', 283 | }, 284 | }, 285 | }, 286 | { 287 | id = 5, 288 | itemName = 'pure_muffin', 289 | name = 'Muffin', 290 | image = ImageDirectory..'pure_muffin.png', 291 | description = 'A Muffin', 292 | craftingTime = 6, 293 | requiredItems = { 294 | { 295 | itemName = 'pure_foodingredients', 296 | name = 'Food Ingredients', 297 | amount = 5, 298 | myAmount = 0, -- Ignore 299 | image = ImageDirectory..'pure_foodingredients.png', 300 | }, 301 | }, 302 | }, 303 | { 304 | id = 6, 305 | itemName = 'pure_pizza', 306 | name = 'Pizza', 307 | image = ImageDirectory..'pure_pizza.png', 308 | description = 'A Pizza', 309 | craftingTime = 15, 310 | requiredItems = { 311 | { 312 | itemName = 'pure_foodingredients', 313 | name = 'Food Ingredients', 314 | amount = 15, 315 | myAmount = 0, -- Ignore 316 | image = ImageDirectory..'pure_foodingredients.png', 317 | }, 318 | }, 319 | }, 320 | { 321 | id = 7, 322 | itemName = 'pure_brownie', 323 | name = 'Brownie', 324 | image = ImageDirectory..'pure_brownie.png', 325 | description = 'A Brownie', 326 | craftingTime = 10, 327 | requiredItems = { 328 | { 329 | itemName = 'pure_foodingredients', 330 | name = 'Food Ingredients', 331 | amount = 5, 332 | myAmount = 0, -- Ignore 333 | image = ImageDirectory..'pure_foodingredients.png', 334 | }, 335 | }, 336 | }, 337 | }, 338 | } 339 | 340 | -- DO NOT TOUCH THESE!!!! 341 | Config.categories = { 342 | { 343 | icon = 'fa-star', 344 | category = 'fave', 345 | }, 346 | { 347 | icon = 'fa-pen-ruler', 348 | category = 'blueprints', 349 | }, 350 | } --------------------------------------------------------------------------------