ESX = exports['es_extended']:getSharedObject() -- ===================== -- WEBHOOK HELPERS -- ===================== local function SendOpeningWebhook(title, message, color) if not Config.WebhookOpenings or Config.WebhookOpenings == "" then return end local embed = { { ["color"] = color or 16750848, ["title"] = "**" .. title .. "**", ["description"] = message, ["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") }, } } PerformHttpRequest(Config.WebhookOpenings, function() end, 'POST', json.encode({username = "Cases Log", embeds = embed}), { ['Content-Type'] = 'application/json' }) end local function SendRewardWebhook(title, message, color) if not Config.WebhookRewards or Config.WebhookRewards == "" then return end local embed = { { ["color"] = color or 3447003, ["title"] = "**" .. title .. "**", ["description"] = message, ["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") }, } } PerformHttpRequest(Config.WebhookRewards, function() end, 'POST', json.encode({username = "Cases Rewards", embeds = embed}), { ['Content-Type'] = 'application/json' }) end -- ===================== -- SQL SETUP -- ===================== MySQL.ready(function() MySQL.query.await([[ CREATE TABLE IF NOT EXISTS mercyv_case_history ( id INT AUTO_INCREMENT PRIMARY KEY, identifier VARCHAR(60) NOT NULL, player_name VARCHAR(100) DEFAULT 'Unbekannt', case_type VARCHAR(50) NOT NULL, item_won VARCHAR(100) NOT NULL, item_label VARCHAR(100) NOT NULL, item_amount INT DEFAULT 1, rarity VARCHAR(20) NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_identifier (identifier), INDEX idx_timestamp (timestamp) ) ]]) print('[mercyv-cases] Database ready') end) -- ===================== -- HELPERS -- ===================== local function GetIdentifier(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return nil end return xPlayer.getIdentifier() end -- Rarity-Farbe als Dezimal fuer Discord Embeds local rarityColors = { ['common'] = 10263708, -- #9ca3af ['uncommon'] = 4898432, -- #4ade80 ['rare'] = 3899126, -- #3b82f6 ['epic'] = 11096567, -- #a855f7 ['legendary'] = 16356118, -- #f97316 } -- Gewichtete Zufallsauswahl (Gleichverteilung innerhalb Rarity) -- 1. Rarity wuerfeln (basierend auf Config.Rarities Gewichten) -- 2. Innerhalb der gewonnenen Rarity gleichmaessig ein Item waehlen local function GetRandomItem(caseId) local caseData = Config.Cases[caseId] if not caseData or not caseData.items or #caseData.items == 0 then return nil end -- Items nach Rarity gruppieren local rarityGroups = {} for _, item in ipairs(caseData.items) do if not rarityGroups[item.rarity] then rarityGroups[item.rarity] = {} end rarityGroups[item.rarity][#rarityGroups[item.rarity] + 1] = item end -- Vorhandene Rarities mit Gewichten sammeln local availableRarities = {} local totalWeight = 0 for rarityKey, items in pairs(rarityGroups) do local rarityDef = Config.Rarities[rarityKey] local weight = rarityDef and rarityDef.weight or 1 totalWeight = totalWeight + weight availableRarities[#availableRarities + 1] = { rarity = rarityKey, items = items, cumulativeWeight = totalWeight, } end -- Rarity wuerfeln local roll = math.random() * totalWeight local chosenGroup = availableRarities[#availableRarities] -- Fallback for _, entry in ipairs(availableRarities) do if roll <= entry.cumulativeWeight then chosenGroup = entry break end end -- Innerhalb der Rarity gleichmaessig ein Item waehlen local items = chosenGroup.items return items[math.random(#items)] end -- ===================== -- PENDING REWARDS -- ===================== -- Rewards werden erst gegeben wenn der Spieler die Animation gesehen hat local pendingRewards = {} -- pendingRewards[source] = { wonItem, caseId, caseData, identifier, playerName, rarityData } -- ===================== -- USABLE ITEMS -- ===================== -- Fuer jeden Case in Config ein usable Item registrieren for caseId, caseData in pairs(Config.Cases) do ESX.RegisterUsableItem(caseId, function(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- Pruefen ob bereits ein Reward aussteht if pendingRewards[source] then TriggerClientEvent('mercyv-cases:notify', source, 'Du hast noch einen ausstehenden Gewinn!', 'error') return end local identifier = xPlayer.getIdentifier() local playerName = xPlayer.getName() -- Pruefen ob der Spieler das Item hat local itemCount = 0 local ok, count = pcall(function() return exports['codem-inventory']:GetItemsTotalAmount(source, caseId) end) if ok and count then itemCount = tonumber(count) or 0 end if itemCount < 1 then TriggerClientEvent('mercyv-cases:notify', source, 'Du hast keine ' .. caseData.label .. '!', 'error') return end -- Item entfernen local removeOk = pcall(function() exports['codem-inventory']:RemoveItem(source, caseId, 1) end) if not removeOk then TriggerClientEvent('mercyv-cases:notify', source, 'Fehler beim Entfernen der Kiste!', 'error') return end -- Gewinn berechnen (server-seitig!) local wonItem = GetRandomItem(caseId) if not wonItem then -- Fallback: Item zurueckgeben pcall(function() exports['codem-inventory']:AddItem(source, caseId, 1) end) TriggerClientEvent('mercyv-cases:notify', source, 'Fehler beim Oeffnen der Kiste!', 'error') return end local rarityData = Config.Rarities[wonItem.rarity] or Config.Rarities['common'] -- Reward als ausstehend speichern (wird erst nach Animation gegeben) pendingRewards[source] = { wonItem = wonItem, caseId = caseId, caseData = caseData, identifier = identifier, playerName = playerName, rarityData = rarityData, } -- Oeffnungs-Webhook (Kiste wurde geoeffnet) local openMsg = string.format( "šŸ‘¤ **Spieler:** %s (%s)\nšŸ“¦ **Kiste:** %s %s", playerName, identifier, caseData.emoji, caseData.label ) SendOpeningWebhook("šŸ“¦ Kiste Geoeffnet", openMsg, 16750848) -- Case-Daten fuer NUI vorbereiten (ohne Gewichte!) local caseItems = {} for _, item in ipairs(caseData.items) do local rd = Config.Rarities[item.rarity] or Config.Rarities['common'] caseItems[#caseItems + 1] = { item = item.item, label = item.label, rarity = item.rarity, amount = item.amount, image = item.image or item.item, rarityLabel = rd.label, rarityColor = rd.color, rarityEmoji = rd.emoji, } end -- Gewinn-Daten (fuer die Animation, Item wird NOCH NICHT gegeben!) local wonItemData = { item = wonItem.item, label = wonItem.label, rarity = wonItem.rarity, amount = wonItem.amount, image = wonItem.image or wonItem.item, rarityLabel = rarityData.label, rarityColor = rarityData.color, rarityEmoji = rarityData.emoji, } -- Client Event: UI oeffnen mit Case + Gewinn TriggerClientEvent('mercyv-cases:openUI', source, { caseId = caseId, caseLabel = caseData.label, caseEmoji = caseData.emoji, caseDescription = caseData.description, caseImage = caseData.image, items = caseItems, wonItem = wonItemData, }) end) end -- ===================== -- CLAIM REWARD (nach Animation) -- ===================== RegisterNetEvent('mercyv-cases:claimReward') AddEventHandler('mercyv-cases:claimReward', function() local source = source local pending = pendingRewards[source] if not pending then return end local wonItem = pending.wonItem local rarityData = pending.rarityData -- Gewinn an Spieler geben pcall(function() exports['codem-inventory']:AddItem(source, wonItem.item, wonItem.amount) end) -- DB-Logging MySQL.query('INSERT INTO mercyv_case_history (identifier, player_name, case_type, item_won, item_label, item_amount, rarity) VALUES (?, ?, ?, ?, ?, ?, ?)', { pending.identifier, pending.playerName, pending.caseId, wonItem.item, wonItem.label, wonItem.amount, wonItem.rarity }) -- Belohnungs-Webhook local rewardMsg = string.format( "šŸ‘¤ **Spieler:** %s (%s)\nšŸ“¦ **Kiste:** %s %s\nšŸŽ **Gewinn:** %s %s (x%d)\n⭐ **Seltenheit:** %s %s", pending.playerName, pending.identifier, pending.caseData.emoji, pending.caseData.label, rarityData.emoji, wonItem.label, wonItem.amount, rarityData.emoji, rarityData.label ) SendRewardWebhook("šŸŽ Belohnung Erhalten", rewardMsg, rarityColors[wonItem.rarity] or 3447003) -- Pending loeschen pendingRewards[source] = nil end) -- ===================== -- DISCONNECT HANDLER -- ===================== -- Falls ein Spieler waehrend der Animation disconnected, Reward trotzdem geben AddEventHandler('playerDropped', function() local source = source local pending = pendingRewards[source] if not pending then return end -- Versuchen das Item ueber Identifier zu geben -- Da der Spieler offline ist, direkt in DB einfuegen MySQL.query('INSERT INTO mercyv_case_history (identifier, player_name, case_type, item_won, item_label, item_amount, rarity) VALUES (?, ?, ?, ?, ?, ?, ?)', { pending.identifier, pending.playerName, pending.caseId, pending.wonItem.item, pending.wonItem.label, pending.wonItem.amount, pending.wonItem.rarity }) -- Item kann nicht direkt gegeben werden (Spieler offline) -- Ueber ESX Offline-AddItem falls verfuegbar pcall(function() MySQL.query('UPDATE users SET inventory = JSON_SET(COALESCE(inventory, "{}"), ?, COALESCE(JSON_EXTRACT(inventory, ?), 0) + ?) WHERE identifier = ?', { '$.' .. pending.wonItem.item, '$.' .. pending.wonItem.item, pending.wonItem.amount, pending.identifier }) end) print('[mercyv-cases] Pending reward given to disconnected player: ' .. pending.playerName .. ' (' .. pending.wonItem.label .. ')') pendingRewards[source] = nil end) -- ===================== -- HISTORY CALLBACK -- ===================== ESX.RegisterServerCallback('mercyv-cases:getHistory', function(source, cb) local identifier = GetIdentifier(source) if not identifier then return cb({}) end local rows = MySQL.query.await( 'SELECT * FROM mercyv_case_history WHERE identifier = ? ORDER BY timestamp DESC LIMIT ?', { identifier, Config.MaxHistoryDisplay } ) local history = {} if rows then for _, row in ipairs(rows) do local rd = Config.Rarities[row.rarity] or Config.Rarities['common'] history[#history + 1] = { caseType = row.case_type, itemWon = row.item_won, itemLabel = row.item_label, itemAmount = row.item_amount, rarity = row.rarity, rarityLabel = rd.label, rarityColor = rd.color, rarityEmoji = rd.emoji, timestamp = row.timestamp, } end end cb(history) end)