338 lines
12 KiB
Lua
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)
|