diff --git a/resources/[inventory]/nordi_petbowl/client.lua b/resources/[inventory]/nordi_petbowl/client.lua new file mode 100644 index 000000000..8952448c3 --- /dev/null +++ b/resources/[inventory]/nordi_petbowl/client.lua @@ -0,0 +1,243 @@ +local QBCore = exports['qb-core']:GetCoreObject() +local placedBowls = {} +local currentBowl = nil + +-- Load placed bowls from server +RegisterNetEvent('pet-bowls:client:loadBowls', function(bowls) + for _, bowl in pairs(bowls) do + local coords = json.decode(bowl.coords) + local bowlCoords = vector3(coords.x, coords.y, coords.z) + + -- Create the bowl object + local hash = GetHashKey(bowl.model) + RequestModel(hash) + while not HasModelLoaded(hash) do + Wait(10) + end + + local bowlObject = CreateObject(hash, bowlCoords.x, bowlCoords.y, bowlCoords.z, false, false, false) + SetEntityHeading(bowlObject, coords.w) + FreezeEntityPosition(bowlObject, true) + SetEntityAsMissionEntity(bowlObject, true, true) + + -- Store in local table + placedBowls[bowl.bowl_id] = { + object = bowlObject, + id = bowl.bowl_id, + model = bowl.model, + type = bowl.type, + fillLevel = bowl.fill_level + } + + -- Add target + AddTargetToBowl(bowlObject, bowl.bowl_id, bowl.type) + end +end) + +-- Function to add qb-target to a bowl +function AddTargetToBowl(bowlObject, bowlId, bowlType) + exports['qb-target']:AddTargetEntity(bowlObject, { + options = { + { + type = "client", + icon = "fas fa-hand", + label = "Use Bowl", + action = function() + UseBowl(bowlId, bowlType) + end, + canInteract = function() + return true + end, + }, + { + type = "client", + icon = "fas fa-fill", + label = "Fill Bowl", + action = function() + OpenFillMenu(bowlId, bowlType) + end, + canInteract = function() + return true + end, + }, + { + type = "client", + icon = "fas fa-trash", + label = "Pick Up Bowl", + action = function() + PickUpBowl(bowlId) + end, + canInteract = function() + return true + end, + } + }, + distance = 2.0 + }) +end + +-- Function to use a bowl +function UseBowl(bowlId, bowlType) + local bowl = placedBowls[bowlId] + if not bowl then return end + + -- Check if bowl has content + if bowl.fillLevel <= 0 then + lib.notify(Config.Notifications.bowlEmpty) + return + end + + -- Set animation and progress bar based on bowl type + local progressConfig = bowlType == 'food' and Config.ProgressBar.eating or Config.ProgressBar.drinking + + -- Start progress bar + if lib.progressBar(progressConfig) then + -- Consume from bowl + TriggerServerEvent('pet-bowls:server:consumeBowl', bowlId) + end +end + +-- Function to open fill menu +function OpenFillMenu(bowlId, bowlType) + local bowl = placedBowls[bowlId] + if not bowl then return end + + -- Get fill items for this bowl type + local fillItems = Config.FillItems[bowlType] + local options = {} + + for _, item in pairs(fillItems) do + table.insert(options, { + title = item.label, + description = 'Fill Amount: ' .. item.fillAmount .. '%', + onSelect = function() + FillBowl(bowlId, item.item, item.fillAmount) + end + }) + end + + lib.registerContext({ + id = 'bowl_fill_menu', + title = 'Fill ' .. (bowlType == 'food' and 'Food' or 'Water') .. ' Bowl (' .. bowl.fillLevel .. '%)', + options = options + }) + + lib.showContext('bowl_fill_menu') +end + +-- Function to fill a bowl +function FillBowl(bowlId, itemName, fillAmount) + -- Start progress bar + if lib.progressBar(Config.ProgressBar.filling) then + -- Fill the bowl + TriggerServerEvent('pet-bowls:server:fillBowl', bowlId, itemName, fillAmount) + end +end + +-- Function to pick up a bowl +function PickUpBowl(bowlId) + local bowl = placedBowls[bowlId] + if not bowl then return end + + -- Delete the object and remove from server + if DoesEntityExist(bowl.object) then + DeleteEntity(bowl.object) + end + + TriggerServerEvent('pet-bowls:server:removeBowl', bowlId) + placedBowls[bowlId] = nil +end + +-- Command to place a bowl +RegisterCommand('placebowl', function() + OpenPlaceBowlMenu() +end) + +-- Function to open place bowl menu +function OpenPlaceBowlMenu() + local options = {} + + for _, bowl in pairs(Config.BowlProps) do + table.insert(options, { + title = bowl.label, + description = 'Type: ' .. (bowl.type == 'food' and 'Food Bowl' or 'Water Bowl'), + onSelect = function() + PlaceBowl(bowl) + end + }) + end + + lib.registerContext({ + id = 'place_bowl_menu', + title = 'Place Bowl', + options = options + }) + + lib.showContext('place_bowl_menu') +end + +-- Function to place a bowl +function PlaceBowl(bowlConfig) + local playerPed = PlayerPedId() + local coords = GetEntityCoords(playerPed) + local heading = GetEntityHeading(playerPed) + + -- Create the bowl object + local hash = GetHashKey(bowlConfig.model) + RequestModel(hash) + while not HasModelLoaded(hash) do + Wait(10) + end + + local forward = GetEntityForwardVector(playerPed) + local placementCoords = vector3( + coords.x + forward.x * 0.5, + coords.y + forward.y * 0.5, + coords.z - 0.5 + ) + + local bowlObject = CreateObject(hash, placementCoords.x, placementCoords.y, placementCoords.z, true, false, false) + SetEntityHeading(bowlObject, heading) + PlaceObjectOnGroundProperly(bowlObject) + FreezeEntityPosition(bowlObject, true) + SetEntityAsMissionEntity(bowlObject, true, true) + + -- Generate a unique ID for this bowl + local bowlId = 'bowl_' .. math.random(100000, 999999) .. '_' .. GetGameTimer() + + -- Save to server + local finalCoords = GetEntityCoords(bowlObject) + TriggerServerEvent('pet-bowls:server:placeBowl', bowlId, bowlConfig.model, bowlConfig.type, { + x = finalCoords.x, + y = finalCoords.y, + z = finalCoords.z, + w = heading + }) + + -- Store locally + placedBowls[bowlId] = { + object = bowlObject, + id = bowlId, + model = bowlConfig.model, + type = bowlConfig.type, + fillLevel = 0 + } + + -- Add target + AddTargetToBowl(bowlObject, bowlId, bowlConfig.type) + + lib.notify(Config.Notifications.bowlPlaced) +end + +-- Update bowl fill level +RegisterNetEvent('pet-bowls:client:updateBowlLevel', function(bowlId, newLevel) + if placedBowls[bowlId] then + placedBowls[bowlId].fillLevel = newLevel + end +end) + +-- Initialize +Citizen.CreateThread(function() + -- Request all placed bowls from server + TriggerServerEvent('pet-bowls:server:requestBowls') +end) diff --git a/resources/[inventory]/nordi_petbowl/config.lua b/resources/[inventory]/nordi_petbowl/config.lua new file mode 100644 index 000000000..30183d167 --- /dev/null +++ b/resources/[inventory]/nordi_petbowl/config.lua @@ -0,0 +1,134 @@ +Config = {} + +-- Bowl Props +Config.BowlProps = { + -- Food Bowls + { + model = 'm25_1_prop_m51_dog_bowl_full', + label = 'Futternapf', + type = 'food', + capacity = 100, + consumeAmount = 10, + offset = vector3(0.0, 0.0, 0.0) + }, + -- Water Bowls + { + model = 'apa_mp_h_acc_bowl_ceramic_01', + label = 'Wassernapf', + type = 'water', + capacity = 100, + consumeAmount = 10, + offset = vector3(0.0, 0.0, 0.0) + }, +} + +-- Fill Items +Config.FillItems = { + -- Food Items + food = { + { + item = 'hundefutter', + label = 'Hundefutter', + fillAmount = 100 + }, + { + item = 'katzenfutter', + label = 'Katzenfutter', + fillAmount = 100 + } + }, + -- Water Items + water = { + { + item = 'water_bottle', + label = 'Wasser Flasche', + fillAmount = 100 + } + } +} + +-- Consumption Effects +Config.Effects = { + food = { + hunger = 10, + stress = -5 + }, + water = { + thirst = 10, + stress = -2 + } +} + +-- Progress Bar Settings +Config.ProgressBar = { + eating = { + duration = 5000, + label = 'Essen...', + position = 'bottom', + useWhileDead = false, + canCancel = true, + disable = { + car = true, + move = true, + combat = true + } + }, + drinking = { + duration = 3000, + label = 'Trinken...', + position = 'bottom', + useWhileDead = false, + canCancel = true, + disable = { + car = true, + move = true, + combat = true + } + }, + filling = { + duration = 2000, + label = 'Filling Bowl...', + position = 'bottom', + useWhileDead = false, + canCancel = true, + disable = { + car = true, + move = true, + combat = true + }, + anim = { + dict = 'amb@world_human_gardener_plant@male@base', + clip = 'base', + flag = 49 + } + } +} + +-- Notifications +Config.Notifications = { + bowlPlaced = { + title = 'Bowl System', + description = 'You placed a bowl!', + type = 'success' + }, + bowlFilled = { + title = 'Bowl System', + description = 'You filled the bowl!', + type = 'success' + }, + bowlEmpty = { + title = 'Bowl System', + description = 'This bowl is empty!', + type = 'error' + }, + noItem = { + title = 'Bowl System', + description = 'You don\'t have the required item!', + type = 'error' + }, + consumed = { + title = 'Bowl System', + description = 'You consumed from the bowl!', + type = 'success' + } +} diff --git a/resources/[inventory]/nordi_petbowl/fxmanifest.lua b/resources/[inventory]/nordi_petbowl/fxmanifest.lua new file mode 100644 index 000000000..f4901a6ee --- /dev/null +++ b/resources/[inventory]/nordi_petbowl/fxmanifest.lua @@ -0,0 +1,28 @@ +fx_version 'cerulean' +game 'gta5' +lua54 'yes' + +author 'Your Name' +description 'Pet Bowl System - Place, fill and use food/water bowls' +version '1.0.0' + +shared_scripts { + 'config.lua', + '@ox_lib/init.lua' +} + +client_scripts { + 'client.lua' +} + +server_scripts { + '@oxmysql/lib/MySQL.lua', + 'server.lua' +} + +dependencies { + 'qb-core', + 'qb-target', + 'ox_lib', + 'tgiann-inventory' +} diff --git a/resources/[inventory]/nordi_petbowl/server.lua b/resources/[inventory]/nordi_petbowl/server.lua new file mode 100644 index 000000000..adf5c0c45 --- /dev/null +++ b/resources/[inventory]/nordi_petbowl/server.lua @@ -0,0 +1,183 @@ +local QBCore = exports['qb-core']:GetCoreObject() +local bowls = {} + +-- Initialize +Citizen.CreateThread(function() + -- Load all bowls from database + LoadBowls() +end) + +-- Load bowls from database +function LoadBowls() + MySQL.query('SELECT * FROM pet_bowls', {}, function(results) + if results and #results > 0 then + for _, bowl in pairs(results) do + bowls[bowl.bowl_id] = { + id = bowl.bowl_id, + model = bowl.model, + type = bowl.type, + fillLevel = bowl.fill_level, + coords = bowl.coords + } + end + print('^2[Pet-Bowls]^7 Loaded ' .. #results .. ' bowls from database') + else + print('^2[Pet-Bowls]^7 No bowls found in database') + end + end) +end + +-- Client requests all bowls +RegisterNetEvent('pet-bowls:server:requestBowls', function() + local src = source + TriggerClientEvent('pet-bowls:client:loadBowls', src, bowls) +end) + +-- Place a new bowl +RegisterNetEvent('pet-bowls:server:placeBowl', function(bowlId, model, bowlType, coords) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + if not Player then return end + + -- Save to database + local coordsJson = json.encode(coords) + MySQL.insert('INSERT INTO pet_bowls (bowl_id, model, type, fill_level, coords) VALUES (?, ?, ?, ?, ?)', + {bowlId, model, bowlType, 0, coordsJson}, + function(id) + if id then + -- Save to memory + bowls[bowlId] = { + id = bowlId, + model = model, + type = bowlType, + fillLevel = 0, + coords = coordsJson + } + + -- Notify all clients about the new bowl + TriggerClientEvent('pet-bowls:client:updateBowlLevel', -1, bowlId, 0) + end + end + ) +end) + +-- Fill a bowl +RegisterNetEvent('pet-bowls:server:fillBowl', function(bowlId, itemName, fillAmount) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + if not Player then return end + + -- Check if player has the item + local hasItem = exports["tgiann-inventory"]:GetItemByName(src, itemName) + + if not hasItem or hasItem.amount < 1 then + TriggerClientEvent('ox_lib:notify', src, Config.Notifications.noItem) + return + end + + -- Remove the item + exports["tgiann-inventory"]:RemoveItem(src, itemName, 1) + + -- Update bowl fill level + local bowl = bowls[bowlId] + if bowl then + local newLevel = math.min(100, bowl.fillLevel + fillAmount) + + -- Update in memory + bowl.fillLevel = newLevel + + -- Update in database + MySQL.update('UPDATE pet_bowls SET fill_level = ?, last_refill = CURRENT_TIMESTAMP WHERE bowl_id = ?', + {newLevel, bowlId} + ) + + -- Update all clients + TriggerClientEvent('pet-bowls:client:updateBowlLevel', -1, bowlId, newLevel) + TriggerClientEvent('ox_lib:notify', src, Config.Notifications.bowlFilled) + end +end) + +-- Consume from a bowl +RegisterNetEvent('pet-bowls:server:consumeBowl', function(bowlId) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + if not Player then return end + + -- Get bowl data + local bowl = bowls[bowlId] + if not bowl then return end + + -- Find the bowl config + local bowlConfig = nil + for _, config in pairs(Config.BowlProps) do + if config.model == bowl.model then + bowlConfig = config + break + end + end + + if not bowlConfig then return end + + -- Check if bowl has content + if bowl.fillLevel <= 0 then + TriggerClientEvent('ox_lib:notify', src, Config.Notifications.bowlEmpty) + return + end + + -- Consume from bowl + local consumeAmount = bowlConfig.consumeAmount + local newLevel = math.max(0, bowl.fillLevel - consumeAmount) + + -- Update in memory + bowl.fillLevel = newLevel + + -- Update in database + MySQL.update('UPDATE pet_bowls SET fill_level = ? WHERE bowl_id = ?', + {newLevel, bowlId} + ) + + -- Apply effects to player + local effects = Config.Effects[bowl.type] + if effects then + if effects.hunger then + -- Apply hunger effect + TriggerClientEvent('hud:client:UpdateHunger', src, effects.hunger, true) + end + + if effects.thirst then + -- Apply thirst effect + TriggerClientEvent('hud:client:UpdateThirst', src, effects.thirst, true) + end + + if effects.stress then + -- Apply stress effect + TriggerClientEvent('hud:client:UpdateStress', src, effects.stress) + end + end + + -- Update all clients + TriggerClientEvent('pet-bowls:client:updateBowlLevel', -1, bowlId, newLevel) + TriggerClientEvent('ox_lib:notify', src, Config.Notifications.consumed) +end) + +-- Remove a bowl +RegisterNetEvent('pet-bowls:server:removeBowl', function(bowlId) + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + if not Player then return end + + -- Remove from database + MySQL.query('DELETE FROM pet_bowls WHERE bowl_id = ?', {bowlId}) + + -- Remove from memory + bowls[bowlId] = nil +end) + +-- Register command to place bowls +QBCore.Commands.Add('placebowl', 'Place a pet bowl', {}, false, function(source, args) + TriggerClientEvent('pet-bowls:client:openPlaceBowlMenu', source) +end)