diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index fb2fdbb..cbe3231 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -18,10 +18,14 @@ import { getAllUsers, getUser, hardUpdateSkin, + insertLog, updateManyUsers, + updateSkin, updateUserAvatar, + updateUserCoins, } from "../../database/index.js"; import { client } from "../client.js"; +import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js"; // Constants for the AI rate limiter const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5"); @@ -276,6 +280,90 @@ async function handleAdminCommands(message) { }); }); console.log("Reworked", dbSkins.length, "skins."); + break; + case `${prefix}:cases-test`: + try { + const caseType = args[0] ?? "standard"; + const caseCount = args[1] ?? 1; + + let totalResValue = 0; + let highestSkinPrice = 0; + let priceTiers = { + 0: 0, + 100: 0, + 200: 0, + 300: 0, + 400: 0, + 500: 0, + 600: 0, + 700: 0, + 800: 0, + 900: 0, + 1000: 0, + }; + + for (let i = 0; i < caseCount; i++) { + const skins = await drawCaseContent(caseType); + const result = drawCaseSkin(skins); + totalResValue += result.finalPrice; + if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice; + if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1; + if (result.finalPrice >= 100 && result.finalPrice < 200) priceTiers["100"] += 1; + if (result.finalPrice >= 200 && result.finalPrice < 300) priceTiers["200"] += 1; + if (result.finalPrice >= 300 && result.finalPrice < 400) priceTiers["300"] += 1; + if (result.finalPrice >= 400 && result.finalPrice < 500) priceTiers["400"] += 1; + if (result.finalPrice >= 500 && result.finalPrice < 600) priceTiers["500"] += 1; + if (result.finalPrice >= 600 && result.finalPrice < 700) priceTiers["600"] += 1; + if (result.finalPrice >= 700 && result.finalPrice < 800) priceTiers["700"] += 1; + if (result.finalPrice >= 800 && result.finalPrice < 900) priceTiers["800"] += 1; + if (result.finalPrice >= 900 && result.finalPrice < 1000) priceTiers["900"] += 1; + if (result.finalPrice >= 1000) priceTiers["1000"] += 1; + console.log( + `Case ${i + 1}: Won a skin worth ${result.finalPrice} Flopos, ${caseType}, ${result.updatedSkin.tierRank}`, + ); + } + + console.log(totalResValue / caseCount); + message.reply( + `${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`, + ); + } catch (e) { + console.log(e); + message.reply(`Error during case test: ${e.message}`); + } + case `${prefix}:refund-skins`: + try { + const DBskins = getAllSkins.all(); + for (const skin of DBskins) { + const owner = getUser.get(skin.user_id); + if (owner) { + updateUserCoins.run({ + id: owner.id, + coins: owner.coins + skin.currentPrice, + }); + insertLog.run({ + id: `${skin.uuid}-skin-refund-${Date.now()}`, + user_id: owner.id, + target_user_id: null, + action: "SKIN_REFUND", + coins_amount: skin.currentPrice, + user_new_amount: owner.coins + skin.currentPrice, + }); + } + updateSkin.run({ + uuid: skin.uuid, + user_id: null, + currentPrice: null, + currentLvl: null, + currentChroma: null, + }); + } + message.reply("All skins refunded."); + } catch (e) { + console.log(e); + message.reply(`Error during refund skins ${e.message}`); + } + break; } } diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 9b7e702..97d4e7e 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -4,7 +4,6 @@ import { sleep } from "openai/core"; // --- Database Imports --- import { getAllAkhys, - getAllAvailableSkins, getAllUsers, getLogs, getMarketOffersBySkin, @@ -35,6 +34,7 @@ import { DiscordRequest } from "../../api/discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { emitDataUpdated, socketEmit } from "../socket.js"; import { handleCaseOpening } from "../../utils/marketNotifs.js"; +import { drawCaseContent, drawCaseSkin } from "../../utils/caseOpening.js"; // Create a new router instance const router = express.Router(); @@ -120,114 +120,29 @@ export function apiRoutes(client, io) { router.post("/open-case", async (req, res) => { const { userId, caseType } = req.body; - let caseTypeVal, tierWeights; + let caseTypeVal; switch (caseType) { case "standard": - caseTypeVal = 1; - tierWeights = { - "12683d76-48d7-84a3-4e09-6985794f0445": 50, // Select - "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe - "60bca009-4182-7998-dee7-b8a2558dc369": 15, // Premium - "e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive - "411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra - }; + caseTypeVal = 500; break; case "premium": - caseTypeVal = 2; - tierWeights = { - "12683d76-48d7-84a3-4e09-6985794f0445": 35, // Select - "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe - "60bca009-4182-7998-dee7-b8a2558dc369": 30, // Premium - "e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive - "411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra - }; + caseTypeVal = 750; break; case "ultra": - caseTypeVal = 4; - tierWeights = { - "12683d76-48d7-84a3-4e09-6985794f0445": 33, // Select - "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 28, // Deluxe - "60bca009-4182-7998-dee7-b8a2558dc369": 28, // Premium - "e046854e-406c-37f4-6607-19a9ba8426fc": 8, // Exclusive - "411e4a55-4e59-7757-41f0-86a53f101bb5": 3, // Ultra - }; + caseTypeVal = 1000; break; default: return res.status(400).json({ error: "Invalid case type." }); } const commandUser = getUser.get(userId); if (!commandUser) return res.status(404).json({ error: "User not found." }); - const valoPrice = (parseInt(process.env.VALO_PRICE, 10) || 500) * caseTypeVal; + const valoPrice = caseTypeVal; if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." }); try { - const dbSkins = getAllAvailableSkins.all(); - const filteredSkins = skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)); - filteredSkins.forEach((s) => { - let dbSkin = getSkin.get(s.uuid); - s.tierColor = dbSkin?.tierColor; - }); - filteredSkins.forEach((s) => { - s.weight = tierWeights[s.tierUuid] ?? 1; // fallback if missing - }); + const selectedSkins = await drawCaseContent(caseType); - function weightedSample(arr, count) { - let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0); - const list = [...arr]; - const result = []; - - for (let i = 0; i < count && list.length > 0; i++) { - let r = Math.random() * totalWeight; - let running = 0; - let pickIndex = -1; - - for (let j = 0; j < list.length; j++) { - running += list[j].weight; - if (r <= running) { - pickIndex = j; - break; - } - } - - if (pickIndex < 0) break; - - const picked = list.splice(pickIndex, 1)[0]; - result.push(picked); - - // Subtract removed weight - totalWeight -= picked.weight; - } - - return result; - } - - const selectedSkins = weightedSample(filteredSkins, 100); - - const randomSelectedSkinIndex = Math.floor(Math.random() * (selectedSkins.length - 1)); - const randomSelectedSkinUuid = selectedSkins[randomSelectedSkinIndex].uuid; - - const dbSkin = getSkin.get(randomSelectedSkinUuid); - const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); - if (!randomSkinData) { - throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); - } - - // --- Randomize Level and Chroma --- - const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; - let randomChroma = 1; - if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { - // Ensure chroma is at least 1 and not greater than the number of chromas - randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; - } - - // --- Calculate Price --- - const calculatePrice = () => { - let result = parseFloat(dbSkin.basePrice); - result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2); - result *= 1 + randomChroma / 4; - return parseFloat(result.toFixed(0)); - }; - const finalPrice = calculatePrice(); + const result = drawCaseSkin(selectedSkins); // --- Update Database --- insertLog.run({ @@ -243,19 +158,24 @@ export function apiRoutes(client, io) { coins: commandUser.coins - valoPrice, }); updateSkin.run({ - uuid: randomSkinData.uuid, + uuid: result.randomSkinData.uuid, user_id: userId, - currentLvl: randomLevel, - currentChroma: randomChroma, - currentPrice: finalPrice, + currentLvl: result.randomLevel, + currentChroma: result.randomChroma, + currentPrice: result.finalPrice, }); console.log( - `[${Date.now()}] ${userId} opened a ${caseType} Valorant case and received skin ${randomSelectedSkinUuid}`, + `[${Date.now()}] ${userId} opened a ${caseType} Valorant case and received skin ${result.randomSelectedSkinUuid}`, ); - const updatedSkin = getSkin.get(randomSkinData.uuid); - await handleCaseOpening(caseType, userId, randomSelectedSkinUuid, client); - res.json({ selectedSkins, randomSelectedSkinUuid, randomSelectedSkinIndex, updatedSkin }); + const updatedSkin = getSkin.get(result.randomSkinData.uuid); + await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client); + res.json({ + selectedSkins, + randomSelectedSkinUuid: result.randomSelectedSkinUuid, + randomSelectedSkinIndex: result.randomSelectedSkinIndex, + updatedSkin, + }); } catch (error) { console.error("Error fetching skins:", error); res.status(500).json({ error: "Failed to fetch skins." }); diff --git a/src/utils/caseOpening.js b/src/utils/caseOpening.js new file mode 100644 index 0000000..fcb238a --- /dev/null +++ b/src/utils/caseOpening.js @@ -0,0 +1,129 @@ +import { getAllAvailableSkins, getSkin } from "../database/index.js"; +import { skins } from "../game/state.js"; + +export async function drawCaseContent(caseType = "standard") { + let tierWeights; + switch (caseType) { + case "standard": + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 50, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 19, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 1, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 0, // Ultra + }; + break; + case "premium": + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 25, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 25, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 40, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 8, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 2, // Ultra + }; + break; + case "ultra": + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 0, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 0, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 33, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 33, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 33, // Ultra + }; + break; + default: + break; + } + + try { + const dbSkins = getAllAvailableSkins.all(); + const weightedPool = skins + .filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)) + .map((s) => { + const dbSkin = getSkin.get(s.uuid); + return { + ...s, // Shallow copy to avoid mutating the imported 'skins' object + tierColor: dbSkin?.tierColor, + weight: tierWeights[s.contentTierUuid] ?? 0, + }; + }) + .filter((s) => s.weight > 0); // <--- CRITICAL: Remove 0 weight skins + + function weightedSample(arr, count) { + let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0); + const list = [...arr]; + const result = []; + + // 2. Adjust count if the pool is smaller than requested + const actualCount = Math.min(count, list.length); + + for (let i = 0; i < actualCount; i++) { + let r = Math.random() * totalWeight; + let running = 0; + let pickIndex = -1; + + for (let j = 0; j < list.length; j++) { + running += list[j].weight; + // Changed to strictly less than for safer bounds, + // though filtering weight > 0 above is the primary fix. + if (r <= running) { + pickIndex = j; + break; + } + } + + if (pickIndex < 0) pickIndex = list.length - 1; + + const picked = list.splice(pickIndex, 1)[0]; + result.push(picked); + totalWeight -= picked.weight; + + if (totalWeight <= 0) break; // Stop if no more weight exists + } + + return result; + } + + return weightedSample(weightedPool, 100); + } catch (e) { + console.log(e); + } +} + +export function drawCaseSkin(caseContent) { + const randomSelectedSkinIndex = Math.floor(Math.random() * (caseContent.length - 1)); + const randomSelectedSkinUuid = caseContent[randomSelectedSkinIndex].uuid; + + const dbSkin = getSkin.get(randomSelectedSkinUuid); + const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); + if (!randomSkinData) { + throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); + } + + // --- Randomize Level and Chroma --- + const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; + let randomChroma = 1; + if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { + // Ensure chroma is at least 1 and not greater than the number of chromas + randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; + } + + // --- Calculate Price --- + const calculatePrice = () => { + let result = parseFloat(dbSkin.basePrice); + result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2); + result *= 1 + randomChroma / 4; + return parseFloat(result.toFixed(0)); + }; + const finalPrice = calculatePrice(); + + return { + caseContent, + finalPrice, + randomLevel, + randomChroma, + randomSkinData, + randomSelectedSkinUuid, + randomSelectedSkinIndex, + }; +}