mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 13:30:36 +01:00
cs skins
This commit is contained in:
44
prisma/migrations/20260301140605_add_cs_skins/migration.sql
Normal file
44
prisma/migrations/20260301140605_add_cs_skins/migration.sql
Normal file
@@ -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;
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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." });
|
||||
|
||||
@@ -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,
|
||||
|
||||
36
src/services/csSkin.service.js
Normal file
36
src/services/csSkin.service.js
Normal file
@@ -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 } } });
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
|
||||
},
|
||||
{
|
||||
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: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
|
||||
},
|
||||
{
|
||||
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: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
|
||||
},
|
||||
{
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user