diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c94731..3ad6c4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,12 +31,18 @@ importers: express: specifier: ^4.18.2 version: 4.22.1 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 node-cron: specifier: ^3.0.3 version: 3.0.3 openai: specifier: ^4.104.0 version: 4.104.0(ws@8.19.0)(zod@4.3.6) + pnpm: + specifier: ^10.29.2 + version: 10.30.2 pokersolver: specifier: ^2.1.4 version: 2.1.4 @@ -1105,6 +1111,11 @@ packages: resolution: { integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== } + jsonwebtoken@9.0.3: + resolution: + { integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== } + engines: { node: ">=12", npm: ">=6" } + jwa@2.0.1: resolution: { integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== } @@ -1127,10 +1138,38 @@ packages: { integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== } engines: { node: ">=10" } + lodash.includes@4.3.0: + resolution: + { integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== } + + lodash.isboolean@3.0.3: + resolution: + { integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== } + + lodash.isinteger@4.0.4: + resolution: + { integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== } + + lodash.isnumber@3.0.3: + resolution: + { integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== } + + lodash.isplainobject@4.0.6: + resolution: + { integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== } + + lodash.isstring@4.0.1: + resolution: + { integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== } + lodash.merge@4.6.2: resolution: { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== } + lodash.once@4.1.1: + resolution: + { integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== } + lodash.snakecase@4.1.1: resolution: { integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== } @@ -1361,6 +1400,12 @@ packages: resolution: { integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig== } + pnpm@10.30.2: + resolution: + { integrity: sha512-Ns3HB+e3lAqYjJwez4jQhPhRS1w/CF9TouJEwpIdOyVFvCDdTr4fwkX+7EY7spiuzqemPtH3aAuHfcY3nY0MtA== } + engines: { node: ">=18.12" } + hasBin: true + pokersolver@2.1.4: resolution: { integrity: sha512-vmgZS+K8H8r1RePQykFM5YyvlKo1v3xVec8FMBjg9N6mR2Tj/n/X415w+lG67FWbrk71D/CADmKFinDgaQlAsw== } @@ -2682,6 +2727,19 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -2706,8 +2764,22 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash@4.17.23: {} @@ -2864,6 +2936,8 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + pnpm@10.30.2: {} + pokersolver@2.1.4: {} prelude-ls@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..838de94 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +onlyBuiltDependencies: + - "@prisma/client" + - "@prisma/engines" + - prisma + - protobufjs diff --git a/src/api/cs.js b/src/api/cs.js new file mode 100644 index 0000000..d1b0e93 --- /dev/null +++ b/src/api/cs.js @@ -0,0 +1,57 @@ +import { csSkinsData, csSkinsPrices } from "../utils/cs.state.js"; + +const params = new URLSearchParams({ + app_id: 730, + currency: "EUR", +}); + +export const fetchSuggestedPrices = async () => { + try { + const response = await fetch(`https://api.skinport.com/v1/items?${params}`, { + method: "GET", + headers: { "Accept-Encoding": "br" }, + }); + const data = await response.json(); + data.forEach((skin) => { + if (skin.market_hash_name) { + csSkinsPrices[skin.market_hash_name] = { + suggested_price: skin.suggested_price, + min_price: skin.min_price, + max_price: skin.max_price, + mean_price: skin.mean_price, + median_price: skin.median_price, + }; + } + }); + return data; + } catch (error) { + console.error("Error parsing JSON:", error); + return null; + } +}; + +export const fetchSkinsData = async () => { + try { + const response = await fetch( + `https://raw.githubusercontent.com/ByMykel/CSGO-API/main/public/api/en/skins.json`, + ); + const data = await response.json(); + let rarities = {}; + data.forEach((skin) => { + if (skin.market_hash_name) { + csSkinsData[skin.market_hash_name] = skin; + } else if (skin.name) { + csSkinsData[skin.name] = skin; + } + if (skin.rarity && skin.rarity.name) { + rarities[skin.rarity.name] = (rarities[skin.rarity.name] || 0) + 1; + } + + }); + console.log(rarities) + return data; + } catch (error) { + console.error("Error fetching skins data:", error); + return null; + } +}; diff --git a/src/bot/events.js b/src/bot/events.js index e08e02e..f9d2d4e 100644 --- a/src/bot/events.js +++ b/src/bot/events.js @@ -1,5 +1,6 @@ import { handleMessageCreate } from "./handlers/messageCreate.js"; import { getAkhys } from "../utils/index.js"; +import { fetchSuggestedPrices, fetchSkinsData } from "../api/cs.js"; /** * Initializes and attaches all necessary event listeners to the Discord client. @@ -18,6 +19,8 @@ export function initializeEvents(client, io) { await getAkhys(client); console.log("[Startup] Setting up scheduled tasks..."); //setupCronJobs(client, io); + await fetchSuggestedPrices(); + await fetchSkinsData(); console.log("--- FlopoBOT is fully operational ---"); }); diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index 5ff6261..b900c8e 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -19,6 +19,9 @@ import * as skinService from "../../services/skin.service.js"; import * as logService from "../../services/log.service.js"; import { client } from "../client.js"; import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js"; +import { fetchSuggestedPrices, fetchSkinsData } from "../../api/cs.js"; +import { csSkinsData, csSkinsPrices } from "../../utils/cs.state.js"; +import { getRandomSkinWithRandomSpecs } from "../../utils/cs.utils.js"; // Constants for the AI rate limiter const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5"); @@ -379,6 +382,99 @@ async function handleAdminCommands(message) { message.reply(`Error during refund skins ${e.message}`); } + break; + case `${prefix}:cs-search`: + try { + const searchTerm = args.join(" "); + if (!searchTerm) { + message.reply("Please provide a search term."); + return; + } + const filteredData = csSkinsData + ? Object.values(csSkinsData).filter((skin) => { + const name = skin.market_hash_name.toLowerCase(); + return args.every((word) => name.includes(word.toLowerCase())); + }) + : []; + if (filteredData.length === 0) { + message.reply(`No skins found matching "${searchTerm}".`); + return; + } else if (filteredData.length <= 10) { + const skinList = filteredData + .map( + (skin) => + `${skin.market_hash_name} - ${ + csSkinsPrices[skin.market_hash_name] + ? "Sug " + + csSkinsPrices[skin.market_hash_name].suggested_price + + " | Min " + + csSkinsPrices[skin.market_hash_name].min_price + + " | Max " + + csSkinsPrices[skin.market_hash_name].max_price + + " | Avg " + + csSkinsPrices[skin.market_hash_name].mean_price + + " | Med " + + csSkinsPrices[skin.market_hash_name].median_price + : "N/A" + }`, + ) + .join("\n"); + message.reply(`Skins matching "${searchTerm}":\n${skinList}`); + } else { + message.reply(`Found ${filteredData.length} skins matching "${searchTerm}".`); + } + } catch (e) { + console.log(e); + message.reply(`Error searching CS:GO skins: ${e.message}`); + } + break; + case `${prefix}:open-cs`: + try { + const randomSkin = getRandomSkinWithRandomSpecs(args[0] ? parseFloat(args[0]) : null); + message.reply( + `You opened a CS:GO case and got: ${randomSkin.name} (${randomSkin.data.rarity.name}, ${ + randomSkin.isStattrak ? "StatTrak, " : "" + }${randomSkin.isSouvenir ? "Souvenir, " : ""}${randomSkin.wearState} - float ${randomSkin.float})\nBase Price: ${ + randomSkin.price ?? "N/A" + } Flopos\nImage url: [url](${randomSkin.data.image || "N/A"})`, + ); + } catch (e) { + console.log(e); + message.reply(`Error opening CS:GO case: ${e.message}`); + } + break; + case `${prefix}:simulate-cs`: + try { + const caseCount = parseInt(args[0]) || 100; + const caseType = args[1] || "default"; + let totalResValue = 0; + let highestSkinPrice = 0; + const priceTiers = { + "Consumer Grade": 0, + "Industrial Grade": 0, + "Mil-Spec Grade": 0, + "Restricted": 0, + "Classified": 0, + "Covert": 0, + "Extraordinary": 0, + }; + + for (let i = 0; i < caseCount; i++) { + const result = getRandomSkinWithRandomSpecs(); + totalResValue += parseInt(result.price); + if (parseInt(result.price) > highestSkinPrice) { + highestSkinPrice = parseInt(result.price); + } + priceTiers[result.data.rarity.name]++; + } + 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 simulation: ${e.message}`); + } break; } } diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 1dc5ad5..b7e9499 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -1297,10 +1297,6 @@ export function apiRoutes(client, io) { }, }); - console.log( - `[CHECKOUT] New session for user ${userId}: ${session.id}, offer: ${offer.id} (${offer.coins} coins for ${offer.amount_cents} cents)`, - ); - res.json({ sessionId: session.id, url: session.url }); } catch (error) { console.error("Error creating checkout session:", error); diff --git a/src/server/routes/auth.js b/src/server/routes/auth.js index 0d0397c..6ce8d44 100644 --- a/src/server/routes/auth.js +++ b/src/server/routes/auth.js @@ -23,7 +23,7 @@ router.get("/discord", (req, res) => { response_type: "code", scope: "identify", }); - console.log("Redirecting to Discord OAuth2 with params:", params.toString()); + res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`); }); @@ -105,7 +105,7 @@ router.get("/me", async (req, res) => { const user = await userService.getUser(payload.discordId); if (!user) { console.warn("User not found for Discord ID in token:", payload.discordId); - return res.json({discordId: payload.discordId}); + return res.json({ discordId: payload.discordId }); } res.json({ user, discordId: user.id }); diff --git a/src/server/socket.js b/src/server/socket.js index 78c3f92..b222ede 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -264,12 +264,15 @@ async function onSnakeGameStateUpdate(client, eventData) { winnerId = lobby.p2.id; } // If scores are equal, winnerId remains null (draw) + io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId }); await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score }); } else if (lobby.p1.win || lobby.p2.win) { // One player won by filling the grid const winnerId = lobby.p1.win ? lobby.p1.id : lobby.p2.id; + io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId }); await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score }); } + delete activeSnakeGames[lobby.gameKey]; } export async function onGameOver(client, gameType, playerId, winnerId, reason = "", scores = null) { @@ -284,14 +287,17 @@ export async function onGameOver(client, gameType, playerId, winnerId, reason = await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase(), scores); resultText = "Égalité"; } else { - await eloHandler( - game.p1.id, - game.p2.id, - game.p1.id === winnerId ? 1 : 0, - game.p2.id === winnerId ? 1 : 0, - title.toUpperCase(), - scores, - ); + // Temp fix: Don't update ELO for Snake since it's not in a stable state yet. + if (gameType !== "snake") { + await eloHandler( + game.p1.id, + game.p2.id, + game.p1.id === winnerId ? 1 : 0, + game.p2.id === winnerId ? 1 : 0, + title.toUpperCase(), + scores, + ); + } const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name; resultText = `Victoire de ${winnerName}`; } diff --git a/src/utils/cs.state.js b/src/utils/cs.state.js new file mode 100644 index 0000000..855aa50 --- /dev/null +++ b/src/utils/cs.state.js @@ -0,0 +1,3 @@ +export let csSkinsData = {}; + +export let csSkinsPrices = {}; diff --git a/src/utils/cs.utils.js b/src/utils/cs.utils.js new file mode 100644 index 0000000..a05333e --- /dev/null +++ b/src/utils/cs.utils.js @@ -0,0 +1,128 @@ +import { csSkinsData, csSkinsPrices } from "./cs.state.js"; + +const StateFactoryNew = "Factory New"; +const StateMinimalWear = "Minimal Wear"; +const StateFieldTested = "Field-Tested"; +const StateWellWorn = "Well-Worn"; +const StateBattleScarred = "Battle-Scarred"; + +export const RarityToColor = { + Gold: 0xffd700, // Standard Gold + Covert: 0xeb4b4b, // Red + Classified: 0xd32ce6, // Pink/Magenta + Restricted: 0x8847ff, // Purple + "Mil-Spec Grade": 0x4b69ff, // Dark Blue + "Industrial Grade": 0x5e98d9, // Light Blue + "Consumer Grade": 0xb0c3d9, // Light Grey/White +}; + +const basePriceRanges = { + "Consumer Grade": { min: 1, max: 5 }, + "Industrial Grade": { min: 2, max: 10 }, + "Mil-Spec Grade": { min: 3, max: 70 }, + "Restricted": { min: 17, max: 400 }, + "Classified": { min: 70, max: 1700 }, + "Covert": { min: 350, max: 17000 }, + "Gold": { min: 10000, max: 100000 }, + "Extraordinary": { min: 10000, max: 100000 }, +}; + +const wearStateMultipliers = { + [StateFactoryNew]: 1, + [StateMinimalWear]: 0.75, + [StateFieldTested]: 0.65, + [StateWellWorn]: 0.6, + [StateBattleScarred]: 0.5, +}; + +export function randomSkinRarity() { + const roll = Math.random(); + + const goldLimit = 0.003; + const covertLimit = goldLimit + 0.014; + const classifiedLimit = covertLimit + 0.04; + const restrictedLimit = classifiedLimit + 0.2; + const milSpecLimit = restrictedLimit + 0.5; + const industrialLimit = milSpecLimit + 0.2; + + if (roll < goldLimit) return "Extraordinary"; + if (roll < covertLimit) return "Covert"; + if (roll < classifiedLimit) return "Classified"; + if (roll < restrictedLimit) return "Restricted"; + if (roll < milSpecLimit) return "Mil-Spec Grade"; + if (roll < industrialLimit) return "Industrial Grade"; + return "Consumer Grade"; +} + +export function generatePrice(rarity, float, isStattrak, isSouvenir, wearState) { + const ranges = basePriceRanges[rarity] || basePriceRanges["Industrial Grade"]; + console.log(ranges) + + let basePrice = ranges.min + (Math.random()) * (ranges.max - ranges.min); + console.log(basePrice) + + const stateMultiplier = wearStateMultipliers[wearState] ?? 1.0; + console.log(stateMultiplier) + + let finalPrice = basePrice * stateMultiplier; + console.log(finalPrice) + + const isExtraordinary = rarity === "Extraordinary"; + + if (isSouvenir && !isExtraordinary) { + finalPrice *= 4 + (Math.random()) * (10.0 - 4); + } else if (isStattrak && !isExtraordinary) { + finalPrice *= 3 + (Math.random()) * (5.0 - 3); + } + console.log(finalPrice) + finalPrice /= 1 + float; // Avoid division by zero and ensure float has a significant impact + + if (finalPrice < 1) finalPrice = 1; + + return finalPrice.toFixed(0); +} + +export function isStattrak(canBeStattrak) { + if (!canBeStattrak) return false; + return Math.random() < 0.15; +} + +export function isSouvenir(canBeSouvenir) { + if (!canBeSouvenir) return false; + return Math.random() < 0.15; +} + +export function getRandomFloatInRange(min, max) { + return min + (Math.random()) * (max - min); +} + +export function getWearState(wear) { + const clamped = Math.max(0.0, Math.min(1.0, wear)); + + if (clamped < 0.07) return StateFactoryNew; + if (clamped < 0.15) return StateMinimalWear; + if (clamped < 0.38) return StateFieldTested; + if (clamped < 0.45) return StateWellWorn; + return StateBattleScarred; +} + +export function getRandomSkinWithRandomSpecs(u_float=null) { + const skinNames = Object.keys(csSkinsData); + const randomRarity = randomSkinRarity(); + console.log(randomRarity) + const filteredSkinNames = skinNames.filter(name => csSkinsData[name].rarity.name === randomRarity); + const randomIndex = Math.floor(Math.random() * filteredSkinNames.length); + + const skinName = filteredSkinNames[randomIndex]; + const skinData = csSkinsData[skinName]; + const float = u_float !== null ? u_float : getRandomFloatInRange(skinData.min_float, skinData.max_float); + return { + name: skinName, + data: skinData, + isStattrak: isStattrak(skinData.stattrak), + isSouvenir: isSouvenir(skinData.souvenir), + wearState: getWearState(float), + float: float, + price: generatePrice(skinData.rarity.name, float, isStattrak(skinData.stattrak), isSouvenir(skinData.souvenir), getWearState(float)), + }; +}