From aeec76e457654fbd5a27ed18cfd8d2c21890682f Mon Sep 17 00:00:00 2001 From: milo Date: Sun, 1 Mar 2026 17:02:51 +0100 Subject: [PATCH] cs skins --- .../20260301140605_add_cs_skins/migration.sql | 44 +++++ prisma/schema.prisma | 32 +++- src/api/cs.js | 10 +- src/bot/commands/inventory.js | 142 +++++++------- src/bot/components/inventoryNav.js | 88 +-------- src/bot/handlers/messageCreate.js | 19 +- src/server/routes/api.js | 175 +++++++++++++++++- src/server/routes/market.js | 36 +++- src/services/csSkin.service.js | 36 ++++ src/services/market.service.js | 33 +++- src/utils/cs.utils.js | 56 +++--- src/utils/index.js | 27 ++- src/utils/marketNotifs.js | 118 ++++++------ 13 files changed, 544 insertions(+), 272 deletions(-) create mode 100644 prisma/migrations/20260301140605_add_cs_skins/migration.sql create mode 100644 src/services/csSkin.service.js diff --git a/prisma/migrations/20260301140605_add_cs_skins/migration.sql b/prisma/migrations/20260301140605_add_cs_skins/migration.sql new file mode 100644 index 0000000..fe3a347 --- /dev/null +++ b/prisma/migrations/20260301140605_add_cs_skins/migration.sql @@ -0,0 +1,44 @@ +-- CreateTable +CREATE TABLE "cs_skins" ( + "id" TEXT NOT NULL PRIMARY KEY, + "market_hash_name" TEXT NOT NULL, + "displayName" TEXT, + "image_url" TEXT, + "rarity" TEXT, + "rarity_color" TEXT, + "weapon_type" TEXT, + "float" REAL, + "wear_state" TEXT, + "is_stattrak" BOOLEAN NOT NULL DEFAULT false, + "is_souvenir" BOOLEAN NOT NULL DEFAULT false, + "price" INTEGER, + "user_id" TEXT, + CONSTRAINT "cs_skins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_market_offers" ( + "id" TEXT NOT NULL PRIMARY KEY, + "skin_uuid" TEXT, + "cs_skin_id" TEXT, + "seller_id" TEXT NOT NULL, + "starting_price" INTEGER NOT NULL, + "buyout_price" INTEGER, + "final_price" INTEGER, + "status" TEXT NOT NULL, + "posted_at" DATETIME DEFAULT CURRENT_TIMESTAMP, + "opening_at" DATETIME NOT NULL, + "closing_at" DATETIME NOT NULL, + "buyer_id" TEXT, + CONSTRAINT "market_offers_skin_uuid_fkey" FOREIGN KEY ("skin_uuid") REFERENCES "skins" ("uuid") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "market_offers_cs_skin_id_fkey" FOREIGN KEY ("cs_skin_id") REFERENCES "cs_skins" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "market_offers_seller_id_fkey" FOREIGN KEY ("seller_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "market_offers_buyer_id_fkey" FOREIGN KEY ("buyer_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_market_offers" ("buyer_id", "buyout_price", "closing_at", "final_price", "id", "opening_at", "posted_at", "seller_id", "skin_uuid", "starting_price", "status") SELECT "buyer_id", "buyout_price", "closing_at", "final_price", "id", "opening_at", "posted_at", "seller_id", "skin_uuid", "starting_price", "status" FROM "market_offers"; +DROP TABLE "market_offers"; +ALTER TABLE "new_market_offers" RENAME TO "market_offers"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index da65b26..721df7c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model User { elo Elo? skins Skin[] + csSkins CsSkin[] @relation("CsSkins") sellerOffers MarketOffer[] @relation("Seller") buyerOffers MarketOffer[] @relation("Buyer") bids Bid[] @@ -56,9 +57,31 @@ model Skin { @@map("skins") } +model CsSkin { + id String @id @default(uuid()) + marketHashName String @map("market_hash_name") + displayName String? + imageUrl String? @map("image_url") + rarity String? + rarityColor String? @map("rarity_color") + weaponType String? @map("weapon_type") + float Float? + wearState String? @map("wear_state") + isStattrak Boolean @default(false) @map("is_stattrak") + isSouvenir Boolean @default(false) @map("is_souvenir") + price Int? + userId String? @map("user_id") + + owner User? @relation("CsSkins", fields: [userId], references: [id]) + marketOffers MarketOffer[] @relation("CsSkinOffers") + + @@map("cs_skins") +} + model MarketOffer { id String @id - skinUuid String @map("skin_uuid") + skinUuid String? @map("skin_uuid") + csSkinId String? @map("cs_skin_id") sellerId String @map("seller_id") startingPrice Int @map("starting_price") buyoutPrice Int? @map("buyout_price") @@ -69,9 +92,10 @@ model MarketOffer { closingAt DateTime @map("closing_at") buyerId String? @map("buyer_id") - skin Skin @relation(fields: [skinUuid], references: [uuid]) - seller User @relation("Seller", fields: [sellerId], references: [id]) - buyer User? @relation("Buyer", fields: [buyerId], references: [id]) + skin Skin? @relation(fields: [skinUuid], references: [uuid]) + csSkin CsSkin? @relation("CsSkinOffers", fields: [csSkinId], references: [id]) + seller User @relation("Seller", fields: [sellerId], references: [id]) + buyer User? @relation("Buyer", fields: [buyerId], references: [id]) bids Bid[] @@map("market_offers") diff --git a/src/api/cs.js b/src/api/cs.js index d1b0e93..cc99723 100644 --- a/src/api/cs.js +++ b/src/api/cs.js @@ -36,19 +36,13 @@ export const fetchSkinsData = async () => { `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; - } - + csSkinsData[skin.name] = skin; + } }); - console.log(rarities) return data; } catch (error) { console.error("Error fetching skins data:", error); diff --git a/src/bot/commands/inventory.js b/src/bot/commands/inventory.js index 0cb61d8..73f73b7 100644 --- a/src/bot/commands/inventory.js +++ b/src/bot/commands/inventory.js @@ -6,10 +6,12 @@ import { } from "discord-interactions"; import { activeInventories, skins } from "../../game/state.js"; import * as skinService from "../../services/skin.service.js"; +import * as csSkinService from "../../services/csSkin.service.js"; +import { RarityToColor } from "../../utils/cs.utils.js"; /** * Handles the /inventory slash command. - * Displays a paginated, interactive embed of a user's Valorant skin inventory. + * Displays a paginated, interactive embed of a user's skin inventory. * * @param {object} req - The Express request object. * @param {object} res - The Express response object. @@ -26,16 +28,22 @@ export async function handleInventoryCommand(req, res, client, interactionId) { }); const { member, guild_id, token, data } = req.body; const commandUserId = member.user.id; - // User can specify another member, otherwise it defaults to themself const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId; try { - // --- 1. Fetch Data --- const guild = await client.guilds.fetch(guild_id); const targetMember = await guild.members.fetch(targetUserId); - const inventorySkins = await skinService.getUserInventory(targetUserId); - // --- 2. Handle Empty Inventory --- + // Fetch both Valorant and CS2 inventories + const valoSkins = await skinService.getUserInventory(targetUserId); + const csSkins = await csSkinService.getUserCsInventory(targetUserId); + + // Combine into a unified list with a type marker + const inventorySkins = [ + ...csSkins.map((s) => ({ ...s, _type: "cs" })), + ...valoSkins.map((s) => ({ ...s, _type: "valo" })), + ]; + if (inventorySkins.length === 0) { return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -44,64 +52,30 @@ export async function handleInventoryCommand(req, res, client, interactionId) { { title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, description: "Cet inventaire est vide.", - color: 0x4f545c, // Discord Gray + color: 0x4f545c, }, ], }, }); } - // --- 3. Store Interactive Session State --- - // This state is crucial for the component handlers to know which inventory to update. activeInventories[interactionId] = { - akhyId: targetUserId, // The inventory owner - userId: commandUserId, // The user who ran the command + akhyId: targetUserId, + userId: commandUserId, page: 0, amount: inventorySkins.length, endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`, timestamp: Date.now(), - inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn + inventorySkins: inventorySkins, }; - // --- 4. Prepare Embed Content --- const currentSkin = inventorySkins[0]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } - const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); + const totalPrice = inventorySkins.reduce((sum, skin) => { + return sum + (skin._type === "cs" ? skin.price || 0 : skin.currentPrice || 0); + }, 0); - // --- Helper functions for formatting --- - const getChromaText = (skin, skinInfo) => { - let result = ""; - for (let i = 1; i <= skinInfo.chromas.length; i++) { - result += skin.currentChroma === i ? "💠 " : "◾ "; - } - return result || "N/A"; - }; + const embed = buildSkinEmbed(currentSkin, targetMember, 1, inventorySkins.length, totalPrice); - const getChromaName = (skin, skinInfo) => { - if (skin.currentChroma > 1) { - const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName - .replace(/[\r\n]+/g, " ") - .replace(skinInfo.displayName, "") - .trim(); - const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); - return match ? match[1].trim() : name; - } - return "Base"; - }; - - const getImageUrl = (skin, skinInfo) => { - if (skin.currentLvl === skinInfo.levels.length) { - const chroma = skinInfo.chromas[skin.currentChroma - 1]; - return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; - } - const level = skinInfo.levels[skin.currentLvl - 1]; - return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; - }; - - // --- 5. Build Initial Components (Buttons) --- const components = [ { type: MessageComponentTypes.BUTTON, @@ -117,38 +91,10 @@ export async function handleInventoryCommand(req, res, client, interactionId) { }, ]; - const isUpgradable = - currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; - // Only show upgrade button if the skin is upgradable AND the command user owns the inventory - if (isUpgradable && targetUserId === commandUserId) { - components.push({ - type: MessageComponentTypes.BUTTON, - custom_id: `upgrade_${interactionId}`, - label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`, - style: ButtonStyleTypes.PRIMARY, - }); - } - - // --- 6. Send Final Response --- return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: { - embeds: [ - { - title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, - color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, - footer: { - text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`, - }, - fields: [ - { - name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, - value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, - }, - ], - image: { url: getImageUrl(currentSkin, skinData) }, - }, - ], + embeds: [embed], components: [ { type: MessageComponentTypes.ACTION_ROW, components: components }, { @@ -170,3 +116,47 @@ export async function handleInventoryCommand(req, res, client, interactionId) { return res.status(500).json({ error: "Failed to generate inventory." }); } } + +/** + * Builds an embed for a single skin (CS2 or Valorant). + */ +export function buildSkinEmbed(skin, targetMember, page, total, totalPrice) { + if (skin._type === "cs") { + const badges = [ + skin.isStattrak ? "StatTrak™" : null, + skin.isSouvenir ? "Souvenir" : null, + ].filter(Boolean).join(" | "); + + return { + title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, + color: RarityToColor[skin.rarity] || 0xf2f3f3, + footer: { + text: `Page ${page}/${total} | Valeur Totale : ${totalPrice} Flopos`, + }, + fields: [ + { + name: `${skin.displayName} | ${skin.price} Flopos`, + value: `${skin.rarity}${badges ? ` | ${badges}` : ""}\n${skin.wearState} (float: ${skin.float?.toFixed(8)})\n${skin.weaponType || ""}`, + }, + ], + image: skin.imageUrl ? { url: skin.imageUrl } : undefined, + }; + } + + // Valorant skin fallback + const skinData = skins.find((s) => s.uuid === skin.uuid); + return { + title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, + color: parseInt(skin.tierColor, 16) || 0xf2f3f3, + footer: { + text: `Page ${page}/${total} | Valeur Totale : ${totalPrice} Flopos`, + }, + fields: [ + { + name: `${skin.displayName} | ${(skin.currentPrice || 0).toFixed(0)} Flopos`, + value: `${skin.tierText || "Valorant"}\nLvl : **${skin.currentLvl}**/${skinData?.levels?.length || "?"}`, + }, + ], + image: skinData ? { url: skinData.displayIcon } : undefined, + }; +} diff --git a/src/bot/components/inventoryNav.js b/src/bot/components/inventoryNav.js index d1e2910..1cb71a9 100644 --- a/src/bot/components/inventoryNav.js +++ b/src/bot/components/inventoryNav.js @@ -6,7 +6,8 @@ import { } from "discord-interactions"; import { DiscordRequest } from "../../api/discord.js"; -import { activeInventories, skins } from "../../game/state.js"; +import { activeInventories } from "../../game/state.js"; +import { buildSkinEmbed } from "../commands/inventory.js"; /** * Handles navigation button clicks (Previous/Next) for the inventory embed. @@ -18,13 +19,10 @@ export async function handleInventoryNav(req, res, client) { const { member, data, guild_id } = req.body; const { custom_id } = data; - // Extract direction ('prev' or 'next') and the original interaction ID from the custom_id const [direction, page, interactionId] = custom_id.split("_"); - // --- 1. Retrieve the interactive session --- const inventorySession = activeInventories[interactionId]; - // --- 2. Validation Checks --- if (!inventorySession) { return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -35,7 +33,6 @@ export async function handleInventoryNav(req, res, client) { }); } - // Ensure the user clicking the button is the one who initiated the command if (inventorySession.userId !== member.user.id) { return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -46,7 +43,6 @@ export async function handleInventoryNav(req, res, client) { }); } - // --- 3. Update Page Number --- const { amount } = inventorySession; if (direction === "next") { inventorySession.page = (inventorySession.page + 1) % amount; @@ -55,49 +51,18 @@ export async function handleInventoryNav(req, res, client) { } try { - // --- 4. Rebuild Embed with New Page Content --- - const { page, inventorySkins } = inventorySession; - const currentSkin = inventorySkins[page]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } + const { inventorySkins } = inventorySession; + const currentPage = inventorySession.page; + const currentSkin = inventorySkins[currentPage]; const guild = await client.guilds.fetch(guild_id); const targetMember = await guild.members.fetch(inventorySession.akhyId); - const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); + const totalPrice = inventorySkins.reduce((sum, skin) => { + return sum + (skin._type === "cs" ? skin.price || 0 : skin.currentPrice || 0); + }, 0); - // --- Helper functions for formatting --- - const getChromaText = (skin, skinInfo) => { - let result = ""; - for (let i = 1; i <= skinInfo.chromas.length; i++) { - result += skin.currentChroma === i ? "💠 " : "◾ "; - } - return result || "N/A"; - }; + const embed = buildSkinEmbed(currentSkin, targetMember, currentPage + 1, amount, totalPrice); - const getChromaName = (skin, skinInfo) => { - if (skin.currentChroma > 1) { - const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName - .replace(/[\r\n]+/g, " ") - .replace(skinInfo.displayName, "") - .trim(); - const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); - return match ? match[1].trim() : name; - } - return "Base"; - }; - - const getImageUrl = (skin, skinInfo) => { - if (skin.currentLvl === skinInfo.levels.length) { - const chroma = skinInfo.chromas[skin.currentChroma - 1]; - return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; - } - const level = skinInfo.levels[skin.currentLvl - 1]; - return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; - }; - - // --- 5. Rebuild Components (Buttons) --- let components = [ { type: MessageComponentTypes.BUTTON, @@ -113,38 +78,10 @@ export async function handleInventoryNav(req, res, client) { }, ]; - const isUpgradable = - currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; - // Conditionally add the upgrade button - if (isUpgradable && inventorySession.akhyId === inventorySession.userId) { - components.push({ - type: MessageComponentTypes.BUTTON, - custom_id: `upgrade_${interactionId}`, - label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`, - style: ButtonStyleTypes.PRIMARY, - }); - } - - // --- 6. Send PATCH Request to Update the Message --- await DiscordRequest(inventorySession.endpoint, { method: "PATCH", body: { - embeds: [ - { - title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, - color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, - footer: { - text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`, - }, - fields: [ - { - name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, - value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, - }, - ], - image: { url: getImageUrl(currentSkin, skinData) }, - }, - ], + embeds: [embed], components: [ { type: MessageComponentTypes.ACTION_ROW, components: components }, { @@ -162,14 +99,9 @@ export async function handleInventoryNav(req, res, client) { }, }); - // --- 7. Acknowledge the Interaction --- - // This tells Discord the interaction was received, and since the message is already updated, - // no further action is needed. return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); } catch (error) { console.error("Error handling inventory navigation:", error); - // In case of an error, we should still acknowledge the interaction to prevent it from failing. - // We can send a silent, ephemeral error message. return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: { diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index b900c8e..af5e843 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -21,7 +21,8 @@ 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"; +import { getRandomSkinWithRandomSpecs, RarityToColor } from "../../utils/cs.utils.js"; +import * as csSkinService from "../../services/csSkin.service.js"; // Constants for the AI rate limiter const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5"); @@ -431,12 +432,26 @@ async function handleAdminCommands(message) { case `${prefix}:open-cs`: try { const randomSkin = getRandomSkinWithRandomSpecs(args[0] ? parseFloat(args[0]) : null); + const created = await csSkinService.insertCsSkin({ + marketHashName: randomSkin.name, + displayName: randomSkin.data.name || randomSkin.name, + imageUrl: randomSkin.data.image || null, + rarity: randomSkin.data.rarity.name, + rarityColor: RarityToColor[randomSkin.data.rarity.name]?.toString(16) || null, + weaponType: randomSkin.data.weapon?.name || null, + float: randomSkin.float, + wearState: randomSkin.wearState, + isStattrak: randomSkin.isStattrak, + isSouvenir: randomSkin.isSouvenir, + price: parseInt(randomSkin.price), + userId: message.author.id, + }); 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"})`, + } Flopos\nSkin ID: ${created.id}\nImage url: [url](${randomSkin.data.image || "N/A"})`, ); } catch (e) { console.log(e); diff --git a/src/server/routes/api.js b/src/server/routes/api.js index b7e9499..b153dd3 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -9,6 +9,7 @@ import * as skinService from "../../services/skin.service.js"; import * as logService from "../../services/log.service.js"; import * as transactionService from "../../services/transaction.service.js"; import * as marketService from "../../services/market.service.js"; +import * as csSkinService from "../../services/csSkin.service.js"; // --- Game State Imports --- import { activePolls, activePredis, activeSlowmodes, skins, activeSnakeGames } from "../../game/state.js"; @@ -23,6 +24,7 @@ import { emitDataUpdated, socketEmit, onGameOver } from "../socket.js"; import { handleCaseOpening } from "../../utils/marketNotifs.js"; import { drawCaseContent, drawCaseSkin, getSkinUpgradeProbs } from "../../utils/caseOpening.js"; import { requireAuth } from "../middleware/auth.js"; +import { getRandomSkinWithRandomSpecs, RarityToColor, TRADE_UP_MAP } from "../../utils/cs.utils.js"; // Create a new router instance const router = express.Router(); @@ -181,6 +183,137 @@ export function apiRoutes(client, io) { } }); + router.post("/open-cs-case", requireAuth, async (req, res) => { + const userId = req.userId; + const casePrice = parseInt(process.env.CS_CASE_PRICE) || 250; + + const commandUser = await userService.getUser(userId); + if (!commandUser) return res.status(404).json({ error: "User not found." }); + if (commandUser.coins < casePrice) return res.status(403).json({ error: "Not enough FlopoCoins." }); + + try { + const randomSkin = getRandomSkinWithRandomSpecs(null, "Covert"); + + const created = await csSkinService.insertCsSkin({ + marketHashName: randomSkin.name, + displayName: randomSkin.data.name || randomSkin.name, + imageUrl: randomSkin.data.image || null, + rarity: randomSkin.data.rarity.name, + rarityColor: RarityToColor[randomSkin.data.rarity.name]?.toString(16) || null, + weaponType: randomSkin.data.weapon?.name || null, + float: randomSkin.float, + wearState: randomSkin.wearState, + isStattrak: randomSkin.isStattrak, + isSouvenir: randomSkin.isSouvenir, + price: parseInt(randomSkin.price), + userId: userId, + }); + + await logService.insertLog({ + id: `${userId}-${Date.now()}`, + userId: userId, + action: "CS_CASE_OPEN", + targetUserId: null, + coinsAmount: -casePrice, + userNewAmount: commandUser.coins - casePrice, + }); + await userService.updateUserCoins(userId, commandUser.coins - casePrice); + + // Generate roulette decoy skins for the animation + const ROULETTE_SIZE = 50; + const resultIndex = 12 + Math.floor(Math.random() * 5); // Place result around index 12-16 + const rouletteSkins = []; + for (let i = 0; i < ROULETTE_SIZE; i++) { + if (i === resultIndex) { + rouletteSkins.push({ + displayName: created.displayName, + imageUrl: created.imageUrl, + rarity: created.rarity, + rarityColor: created.rarityColor, + }); + } else { + const decoy = getRandomSkinWithRandomSpecs(); + rouletteSkins.push({ + displayName: decoy.data.name || decoy.name, + imageUrl: decoy.data.image || null, + rarity: decoy.data.rarity.name, + rarityColor: RarityToColor[decoy.data.rarity.name]?.toString(16) || null, + }); + } + } + + res.json({ skin: created, rouletteSkins, resultIndex }); + } catch (error) { + console.error("Error opening CS case:", error); + res.status(500).json({ error: "Failed to open CS case." }); + } + }); + + router.post("/trade-up", requireAuth, async (req, res) => { + const userId = req.userId; + const { skinIds } = req.body; + + if (!Array.isArray(skinIds) || skinIds.length !== 10) { + return res.status(400).json({ error: "You must provide exactly 10 skin IDs." }); + } + + try { + const skins = await Promise.all(skinIds.map((id) => csSkinService.getCsSkin(id))); + + // Validate all skins exist and are owned by the user + for (const skin of skins) { + if (!skin) return res.status(404).json({ error: "One or more skins not found." }); + if (skin.userId !== userId) return res.status(403).json({ error: "You don't own all of these skins." }); + } + + // Validate all skins are the same rarity + const rarity = skins[0].rarity; + if (!skins.every((s) => s.rarity === rarity)) { + return res.status(400).json({ error: "All 10 skins must be the same rarity." }); + } + + // Validate rarity can be traded up + const nextRarity = TRADE_UP_MAP[rarity]; + if (!nextRarity) { + return res.status(400).json({ error: `${rarity} skins cannot be used in trade-up contracts.` }); + } + + // Delete the 10 input skins + await csSkinService.deleteManyCsSkins(skinIds); + + // Generate a new skin at the next rarity tier + const newSkin = getRandomSkinWithRandomSpecs(null, nextRarity); + const created = await csSkinService.insertCsSkin({ + marketHashName: newSkin.name, + displayName: newSkin.data.name || newSkin.name, + imageUrl: newSkin.data.image || null, + rarity: newSkin.data.rarity.name, + rarityColor: RarityToColor[newSkin.data.rarity.name]?.toString(16) || null, + weaponType: newSkin.data.weapon?.name || null, + float: newSkin.float, + wearState: newSkin.wearState, + isStattrak: newSkin.isStattrak, + isSouvenir: newSkin.isSouvenir, + price: parseInt(newSkin.price), + userId: userId, + }); + + await logService.insertLog({ + id: `${userId}-${Date.now()}`, + userId: userId, + action: "CS_TRADE_UP", + targetUserId: null, + coinsAmount: 0, + userNewAmount: (await userService.getUser(userId)).coins, + }); + + res.json({ skin: created, consumedRarity: rarity, resultRarity: nextRarity }); + } catch (error) { + console.error("Error during trade-up:", error); + res.status(500).json({ error: "Failed to complete trade-up contract." }); + } + }); + router.get("/case-content/:type", async (req, res) => { const { type } = req.params; try { @@ -283,6 +416,44 @@ export function apiRoutes(client, io) { } }); + router.post("/cs-skin/:id/instant-sell", requireAuth, async (req, res) => { + const userId = req.userId; + try { + const skin = await csSkinService.getCsSkin(req.params.id); + if (!skin) return res.status(404).json({ error: "CS skin not found." }); + if (skin.userId !== userId) return res.status(403).json({ error: "User does not own this skin." }); + + const marketOffers = await marketService.getMarketOffersByCsSkin(skin.id); + const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open"); + if (activeOffers.length > 0) { + return res + .status(403) + .json({ error: "Impossible de vendre ce skin, une offre FlopoMarket est déjà en cours." }); + } + + const commandUser = await userService.getUser(userId); + if (!commandUser) return res.status(404).json({ error: "User not found." }); + + const sellPrice = skin.price; + await logService.insertLog({ + id: `${userId}-${Date.now()}`, + userId: userId, + action: "CS_SKIN_INSTANT_SELL", + targetUserId: null, + coinsAmount: sellPrice, + userNewAmount: commandUser.coins + sellPrice, + }); + await userService.updateUserCoins(userId, commandUser.coins + sellPrice); + await csSkinService.deleteCsSkin(skin.id); + + console.log(`${commandUser.username} instantly sold CS skin ${skin.displayName} for ${sellPrice} FlopoCoins`); + res.status(200).json({ sellPrice }); + } catch (error) { + console.error("Error selling CS skin:", error); + res.status(500).json({ error: "Failed to sell CS skin." }); + } + }); + router.get("/skin-upgrade/:uuid/fetch", async (req, res) => { try { const skin = await skinService.getSkin(req.params.uuid); @@ -501,7 +672,9 @@ export function apiRoutes(client, io) { skin.isChampions = isChampionsSkin(skin.displayName); skin.vctRegion = getVCTRegion(skin.displayName); } - res.json({ inventory }); + + const csInventory = await csSkinService.getUserCsInventory(req.params.id); + res.json({ inventory, csInventory }); } catch (error) { console.log(error); res.status(500).json({ error: "Failed to fetch inventory." }); diff --git a/src/server/routes/market.js b/src/server/routes/market.js index 60f7912..d482ebb 100644 --- a/src/server/routes/market.js +++ b/src/server/routes/market.js @@ -9,6 +9,7 @@ import * as userService from "../../services/user.service.js"; import * as skinService from "../../services/skin.service.js"; import * as logService from "../../services/log.service.js"; import * as marketService from "../../services/market.service.js"; +import * as csSkinService from "../../services/csSkin.service.js"; import { emitMarketUpdate } from "../socket.js"; import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js"; import { requireAuth } from "../middleware/auth.js"; @@ -27,7 +28,11 @@ export function marketRoutes(client, io) { try { const offers = await marketService.getMarketOffers(); for (const offer of offers) { - offer.skin = await skinService.getSkin(offer.skinUuid); + if (offer.csSkinId) { + offer.csSkin = await csSkinService.getCsSkin(offer.csSkinId); + } else if (offer.skinUuid) { + offer.skin = await skinService.getSkin(offer.skinUuid); + } offer.seller = await userService.getUser(offer.sellerId); offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null; offer.bids = (await marketService.getOfferBids(offer.id)) || {}; @@ -66,16 +71,30 @@ export function marketRoutes(client, io) { router.post("/place-offer", requireAuth, async (req, res) => { const seller_id = req.userId; - const { skin_uuid, starting_price, delay, duration, timestamp } = req.body; + const { skin_uuid, cs_skin_id, starting_price, delay, duration, timestamp } = req.body; const now = Date.now(); try { - const skin = await skinService.getSkin(skin_uuid); - if (!skin) return res.status(404).send({ error: "Skin not found" }); const seller = await userService.getUser(seller_id); if (!seller) return res.status(404).send({ error: "Seller not found" }); - if (skin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); - const existingOffers = await marketService.getMarketOffersBySkin(skin.uuid); + let skinRef; // { skinUuid, csSkinId } - one or the other + if (cs_skin_id) { + const csSkin = await csSkinService.getCsSkin(cs_skin_id); + if (!csSkin) return res.status(404).send({ error: "CS skin not found" }); + if (csSkin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); + skinRef = { csSkinId: csSkin.id }; + } else if (skin_uuid) { + const skin = await skinService.getSkin(skin_uuid); + if (!skin) return res.status(404).send({ error: "Skin not found" }); + if (skin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); + skinRef = { skinUuid: skin.uuid }; + } else { + return res.status(400).send({ error: "Must provide skin_uuid or cs_skin_id" }); + } + + const existingOffers = skinRef.skinUuid + ? await marketService.getMarketOffersBySkin(skinRef.skinUuid) + : await marketService.getMarketOffersByCsSkin(skinRef.csSkinId); if ( existingOffers.length > 0 && existingOffers.some((offer) => offer.status === "open" || offer.status === "pending") @@ -86,10 +105,11 @@ export function marketRoutes(client, io) { const opening_at = now + delay; const closing_at = opening_at + duration; - const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid; + const offerId = Date.now() + "-" + seller.id + "-" + (skinRef.skinUuid || skinRef.csSkinId); await marketService.insertMarketOffer({ id: offerId, - skinUuid: skin.uuid, + skinUuid: skinRef.skinUuid || null, + csSkinId: skinRef.csSkinId || null, sellerId: seller.id, startingPrice: starting_price, buyoutPrice: null, diff --git a/src/services/csSkin.service.js b/src/services/csSkin.service.js new file mode 100644 index 0000000..76496f1 --- /dev/null +++ b/src/services/csSkin.service.js @@ -0,0 +1,36 @@ +import prisma from "../prisma/client.js"; + +export async function getCsSkin(id) { + return prisma.csSkin.findUnique({ where: { id } }); +} + +export async function getUserCsInventory(userId) { + return prisma.csSkin.findMany({ + where: { userId }, + orderBy: { price: "desc" }, + }); +} + +export async function getUserCsSkinsByRarity(userId, rarity) { + return prisma.csSkin.findMany({ + where: { userId, rarity }, + orderBy: { price: "desc" }, + }); +} + +export async function insertCsSkin(data) { + return prisma.csSkin.create({ data }); +} + +export async function updateCsSkin(data) { + const { id, ...rest } = data; + return prisma.csSkin.update({ where: { id }, data: rest }); +} + +export async function deleteCsSkin(id) { + return prisma.csSkin.delete({ where: { id } }); +} + +export async function deleteManyCsSkins(ids) { + return prisma.csSkin.deleteMany({ where: { id: { in: ids } } }); +} diff --git a/src/services/market.service.js b/src/services/market.service.js index a32e379..2e05441 100644 --- a/src/services/market.service.js +++ b/src/services/market.service.js @@ -14,16 +14,17 @@ export async function getMarketOfferById(id) { where: { id }, include: { skin: { select: { displayName: true, displayIcon: true } }, + csSkin: { select: { displayName: true, imageUrl: true, rarity: true, wearState: true, float: true, isStattrak: true, isSouvenir: true } }, seller: { select: { username: true, globalName: true } }, buyer: { select: { username: true, globalName: true } }, }, }); if (!offer) return null; - // Flatten to match the old query shape + const skinData = offer.csSkin || offer.skin; return toOffer({ ...offer, - skinName: offer.skin?.displayName, - skinIcon: offer.skin?.displayIcon, + skinName: skinData?.displayName, + skinIcon: offer.skin?.displayIcon || offer.csSkin?.imageUrl, sellerName: offer.seller?.username, sellerGlobalName: offer.seller?.globalName, buyerName: offer.buyer?.username ?? null, @@ -53,12 +54,34 @@ export async function getMarketOffersBySkin(skinUuid) { ); } +export async function getMarketOffersByCsSkin(csSkinId) { + const offers = await prisma.marketOffer.findMany({ + where: { csSkinId }, + include: { + csSkin: { select: { displayName: true, imageUrl: true } }, + seller: { select: { username: true, globalName: true } }, + buyer: { select: { username: true, globalName: true } }, + }, + }); + return offers.map((offer) => + toOffer({ + ...offer, + skinName: offer.csSkin?.displayName, + skinIcon: offer.csSkin?.imageUrl, + sellerName: offer.seller?.username, + sellerGlobalName: offer.seller?.globalName, + buyerName: offer.buyer?.username ?? null, + buyerGlobalName: offer.buyer?.globalName ?? null, + }), + ); +} + export async function insertMarketOffer(data) { return prisma.marketOffer.create({ data: { ...data, - openingAt: String(data.openingAt), - closingAt: String(data.closingAt), + openingAt: new Date(data.openingAt), + closingAt: new Date(data.closingAt), }, }); } diff --git a/src/utils/cs.utils.js b/src/utils/cs.utils.js index a05333e..a219011 100644 --- a/src/utils/cs.utils.js +++ b/src/utils/cs.utils.js @@ -35,6 +35,14 @@ const wearStateMultipliers = { [StateBattleScarred]: 0.5, }; +export const TRADE_UP_MAP = { + "Consumer Grade": "Industrial Grade", + "Industrial Grade": "Mil-Spec Grade", + "Mil-Spec Grade": "Restricted", + "Restricted": "Classified", + "Classified": "Covert", +}; + export function randomSkinRarity() { const roll = Math.random(); @@ -42,58 +50,53 @@ export function randomSkinRarity() { 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; + 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"; + 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) + let basePrice = ranges.min + Math.random() * (ranges.max - ranges.min); 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); + finalPrice *= 4 + Math.random() * (10.0 - 4); } else if (isStattrak && !isExtraordinary) { - finalPrice *= 3 + (Math.random()) * (5.0 - 3); + finalPrice *= 3 + Math.random() * (5.0 - 3); } - console.log(finalPrice) - finalPrice /= 1 + float; // Avoid division by zero and ensure float has a significant impact + finalPrice /= 1 + float; if (finalPrice < 1) finalPrice = 1; return finalPrice.toFixed(0); } -export function isStattrak(canBeStattrak) { +export function rollStattrak(canBeStattrak) { if (!canBeStattrak) return false; return Math.random() < 0.15; } -export function isSouvenir(canBeSouvenir) { +export function rollSouvenir(canBeSouvenir) { if (!canBeSouvenir) return false; return Math.random() < 0.15; } export function getRandomFloatInRange(min, max) { - return min + (Math.random()) * (max - min); + return min + Math.random() * (max - min); } export function getWearState(wear) { @@ -106,23 +109,26 @@ export function getWearState(wear) { return StateBattleScarred; } -export function getRandomSkinWithRandomSpecs(u_float=null) { +export function getRandomSkinWithRandomSpecs(u_float, forcedRarity) { const skinNames = Object.keys(csSkinsData); - const randomRarity = randomSkinRarity(); - console.log(randomRarity) - const filteredSkinNames = skinNames.filter(name => csSkinsData[name].rarity.name === randomRarity); + const selectedRarity = forcedRarity || randomSkinRarity(); + const filteredSkinNames = skinNames.filter(name => csSkinsData[name].rarity.name === selectedRarity); 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); + const float = u_float !== null ? u_float : getRandomFloatInRange(skinData.min_float, skinData.max_float); + const wearState = getWearState(float); + const skinIsStattrak = rollStattrak(skinData.stattrak); + const skinIsSouvenir = rollSouvenir(skinData.souvenir); + 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)), + isStattrak: skinIsStattrak, + isSouvenir: skinIsSouvenir, + wearState, + float, + price: generatePrice(skinData.rarity.name, float, skinIsStattrak, skinIsSouvenir, wearState), }; } diff --git a/src/utils/index.js b/src/utils/index.js index fc83048..8f693c4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -8,6 +8,7 @@ import { initTodaysSOTD } from "../game/points.js"; import * as userService from "../services/user.service.js"; import * as skinService from "../services/skin.service.js"; import * as marketService from "../services/market.service.js"; +import * as csSkinService from "../services/csSkin.service.js"; import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js"; import { emitMarketUpdate } from "../server/socket.js"; import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js"; @@ -286,16 +287,22 @@ async function handleMarketOffersUpdate() { const buyer = await userService.getUser(lastBid.bidderId); try { - // Change skin ownership - const skin = await skinService.getSkin(offer.skinUuid); - if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`); - await skinService.updateSkin({ - userId: buyer.id, - currentLvl: skin.currentLvl, - currentChroma: skin.currentChroma, - currentPrice: skin.currentPrice, - uuid: skin.uuid, - }); + // Change skin ownership (supports both Valorant and CS2 skins) + if (offer.csSkinId) { + const csSkin = await csSkinService.getCsSkin(offer.csSkinId); + if (!csSkin) throw new Error(`CS skin not found for offer ID: ${offer.id}`); + await csSkinService.updateCsSkin({ id: csSkin.id, userId: buyer.id }); + } else if (offer.skinUuid) { + const skin = await skinService.getSkin(offer.skinUuid); + if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`); + await skinService.updateSkin({ + userId: buyer.id, + currentLvl: skin.currentLvl, + currentChroma: skin.currentChroma, + currentPrice: skin.currentPrice, + uuid: skin.uuid, + }); + } await marketService.updateMarketOffer({ id: offer.id, buyerId: buyer.id, diff --git a/src/utils/marketNotifs.js b/src/utils/marketNotifs.js index 72a156d..ac6fb28 100644 --- a/src/utils/marketNotifs.js +++ b/src/utils/marketNotifs.js @@ -1,12 +1,28 @@ import * as userService from "../services/user.service.js"; import * as skinService from "../services/skin.service.js"; +import * as csSkinService from "../services/csSkin.service.js"; import * as marketService from "../services/market.service.js"; import { EmbedBuilder } from "discord.js"; +/** + * Gets the skin display name and icon from an offer, supporting both Valorant and CS2 skins. + */ +async function getOfferSkinInfo(offer) { + if (offer.csSkinId) { + const csSkin = await csSkinService.getCsSkin(offer.csSkinId); + return { name: csSkin?.displayName || offer.csSkinId, icon: csSkin?.imageUrl || null }; + } + if (offer.skinUuid) { + const skin = await skinService.getSkin(offer.skinUuid); + return { name: skin?.displayName || offer.skinUuid, icon: skin?.displayIcon || null }; + } + return { name: "Unknown", icon: null }; +} + export async function handleNewMarketOffer(offerId, client) { const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = await skinService.getSkin(offer.skinUuid); + const { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer); const discordUserSeller = await client.users.fetch(offer.sellerId); try { @@ -14,9 +30,8 @@ export async function handleNewMarketOffer(offerId, client) { if (discordUserSeller && userSeller?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Offre créée") - .setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été créée !`) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setDescription(`Ton offre pour le skin **${skinName}** a bien été créée !`) + .setColor(0x5865f2) .addFields( { name: "📌 Statut", @@ -37,27 +52,26 @@ export async function handleNewMarketOffer(offerId, client) { value: ``, }, { - name: "🆔 ID de l’offre", + name: "🆔 ID de l'offre", value: `\`${offer.id}\``, inline: false, }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); discordUserSeller.send({ embeds: [embed] }).catch(console.error); } } catch (e) { console.error(e); } - // Send notification in guild channel try { const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle offre") - .setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a été créée !`) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setDescription(`Une offre pour le skin **${skinName}** a été créée !`) + .setColor(0x5865f2) .addFields( { name: "💰 Prix de départ", @@ -78,6 +92,7 @@ export async function handleNewMarketOffer(offerId, client) { }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); guildChannel.send({ embeds: [embed] }).catch(console.error); } catch (e) { console.error(e); @@ -87,7 +102,7 @@ export async function handleNewMarketOffer(offerId, client) { export async function handleMarketOfferOpening(offerId, client) { const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = await skinService.getSkin(offer.skinUuid); + const { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer); try { const discordUserSeller = await client.users.fetch(offer.sellerId); @@ -96,10 +111,9 @@ export async function handleMarketOfferOpening(offerId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Début des enchères") .setDescription( - `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`, + `Les enchères sur ton offre pour le skin **${skinName}** viennent de commencer !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .addFields( { name: "📌 Statut", @@ -116,29 +130,28 @@ export async function handleMarketOfferOpening(offerId, client) { value: ``, }, { - name: "🆔 ID de l’offre", + name: "🆔 ID de l'offre", value: `\`${offer.id}\``, inline: false, }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); discordUserSeller.send({ embeds: [embed] }).catch(console.error); } } catch (e) { console.error(e); } - // Send notification in guild channel try { const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const embed = new EmbedBuilder() .setTitle("🔔 Début des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`, + `Les enchères sur l'offre pour le skin **${skinName}** viennent de commencer !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .addFields( { name: "💰 Prix de départ", @@ -151,6 +164,7 @@ export async function handleMarketOfferOpening(offerId, client) { }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); guildChannel.send({ embeds: [embed] }).catch(console.error); } catch (e) { console.error(e); @@ -160,7 +174,7 @@ export async function handleMarketOfferOpening(offerId, client) { export async function handleMarketOfferClosing(offerId, client) { const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = await skinService.getSkin(offer.skinUuid); + const { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer); const bids = await marketService.getOfferBids(offer.id); const discordUserSeller = await client.users.fetch(offer.sellerId); @@ -170,11 +184,11 @@ export async function handleMarketOfferClosing(offerId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, + `Les enchères sur ton offre pour le skin **${skinName}** viennent de se terminer !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); if (bids.length === 0) { embed.addFields( @@ -183,7 +197,7 @@ export async function handleMarketOfferClosing(offerId, client) { value: "Tu conserves ce skin dans ton inventaire.", }, { - name: "🆔 ID de l’offre", + name: "🆔 ID de l'offre", value: `\`${offer.id}\``, inline: false, }, @@ -197,7 +211,7 @@ export async function handleMarketOfferClosing(offerId, client) { value: `Ton skin a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, }, { - name: "🆔 ID de l’offre", + name: "🆔 ID de l'offre", value: `\`${offer.id}\``, inline: false, }, @@ -210,19 +224,17 @@ export async function handleMarketOfferClosing(offerId, client) { console.error(e); } - // Send notification in guild channel - try { const guild = await client.guilds.fetch(process.env.BOT_GUILD_ID); const guildChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID); const embed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, + `Les enchères sur l'offre pour le skin **${skinName}** viennent de se terminer !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); if (bids.length === 0) { embed.addFields({ @@ -239,21 +251,20 @@ export async function handleMarketOfferClosing(offerId, client) { const discordUserBidder = await client.users.fetch(highestBid.bidderId); const userBidder = await userService.getUser(highestBid.bidderId); if (discordUserBidder && userBidder?.isAkhy) { - const embed = new EmbedBuilder() + const bidderEmbed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, + `Les enchères sur l'offre pour le skin **${skinName}** viennent de se terminer !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .setTimestamp(); - const highestBid = bids[0]; - embed.addFields({ + if (skinIcon) bidderEmbed.setThumbnail(skinIcon); + bidderEmbed.addFields({ name: "✅ Enchères terminées avec succès !", value: `Tu as acheté ce skin pour \`${highestBid.offerAmount} coins\` à <@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`, }); - discordUserBidder.send({ embeds: [embed] }).catch(console.error); + discordUserBidder.send({ embeds: [bidderEmbed] }).catch(console.error); } } guildChannel.send({ embeds: [embed] }).catch(console.error); @@ -263,12 +274,11 @@ export async function handleMarketOfferClosing(offerId, client) { } export async function handleNewMarketOfferBid(offerId, bidId, client) { - // Notify Seller and Bidder const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; const bid = (await marketService.getOfferBids(offerId))[0]; if (!bid) return; - const skin = await skinService.getSkin(offer.skinUuid); + const { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer); const bidderUser = client.users.fetch(bid.bidderId); try { @@ -279,10 +289,9 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**.`, + `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skinName}**.`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .addFields( { name: "👤 Enchérisseur", @@ -290,7 +299,7 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { inline: true, }, { - name: "💰 Montant de l’enchère", + name: "💰 Montant de l'enchère", value: `\`${bid.offerAmount} coins\``, inline: true, }, @@ -299,12 +308,13 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { value: ``, }, { - name: "🆔 ID de l’offre", + name: "🆔 ID de l'offre", value: `\`${offer.id}\``, inline: false, }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); discordUserSeller.send({ embeds: [embed] }).catch(console.error); } @@ -319,16 +329,16 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été placée!`, + `Ton enchère sur l'offre pour le skin **${skinName}** a bien été placée!`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .addFields({ - name: "💰 Montant de l’enchère", + name: "💰 Montant de l'enchère", value: `\`${bid.offerAmount} coins\``, inline: true, }) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); discordUserNewBidder.send({ embeds: [embed] }).catch(console.error); } @@ -338,7 +348,7 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { try { const offerBids = await marketService.getOfferBids(offer.id); - if (offerBids.length < 2) return; // No previous bidder to notify + if (offerBids.length < 2) return; const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidderId); const userPreviousBidder = await userService.getUser(offerBids[1].bidderId); @@ -346,10 +356,9 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**, tu n'es plus le meilleur enchérisseur !`, + `Quelqu'un a surenchéri sur l'offre pour le skin **${skinName}**, tu n'es plus le meilleur enchérisseur !`, ) - .setThumbnail(skin.displayIcon) - .setColor(0x5865f2) // Discord blurple + .setColor(0x5865f2) .addFields( { name: "👤 Enchérisseur", @@ -357,20 +366,19 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { inline: true, }, { - name: "💰 Montant de l’enchère", + name: "💰 Montant de l'enchère", value: `\`${bid.offerAmount} coins\``, inline: true, }, ) .setTimestamp(); + if (skinIcon) embed.setThumbnail(skinIcon); discordUserPreviousBidder.send({ embeds: [embed] }).catch(console.error); } } catch (e) { console.error(e); } - - // Notify previous highest bidder } export async function handleCaseOpening(caseType, userId, skinUuid, client) { @@ -384,7 +392,7 @@ export async function handleCaseOpening(caseType, userId, skinUuid, client) { `${discordUser ? discordUser.username : "Un utilisateur"} vient d'ouvrir une caisse **${caseType}** et a obtenu le skin **${skin.displayName}** !`, ) .setThumbnail(skin.displayIcon) - .setColor(skin.tierColor) // Discord blurple + .setColor(skin.tierColor) .addFields( { name: "💰 Valeur estimée",