This commit is contained in:
milo
2026-03-01 17:02:51 +01:00
parent c635252758
commit aeec76e457
13 changed files with 544 additions and 272 deletions

View 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;

View File

@@ -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,7 +92,8 @@ model MarketOffer {
closingAt DateTime @map("closing_at")
buyerId String? @map("buyer_id")
skin Skin @relation(fields: [skinUuid], references: [uuid])
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[]

View File

@@ -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;
}
});
console.log(rarities)
return data;
} catch (error) {
console.error("Error fetching skins data:", error);

View File

@@ -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,
};
}

View File

@@ -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: {

View File

@@ -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);

View File

@@ -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." });

View File

@@ -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) {
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,

View 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 } } });
}

View File

@@ -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),
},
});
}

View File

@@ -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();
@@ -56,44 +64,39 @@ export function randomSkinRarity() {
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 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),
};
}

View File

@@ -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,7 +287,12 @@ async function handleMarketOffersUpdate() {
const buyer = await userService.getUser(lastBid.bidderId);
try {
// Change skin ownership
// 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({
@@ -296,6 +302,7 @@ async function handleMarketOffersUpdate() {
currentPrice: skin.currentPrice,
uuid: skin.uuid,
});
}
await marketService.updateMarketOffer({
id: offer.id,
buyerId: buyer.id,

View File

@@ -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 loffre",
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 loffre",
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 loffre",
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 loffre",
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 lenchè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 loffre",
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 lenchè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 lenchè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",