2026-04-14 15:54:53 +02:00

338 lines
12 KiB
Lua

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)