This commit is contained in:
Milo
2026-02-27 17:20:23 +01:00
parent 639e7a9c3c
commit c635252758
10 changed files with 382 additions and 14 deletions

74
pnpm-lock.yaml generated
View File

@@ -31,12 +31,18 @@ importers:
express:
specifier: ^4.18.2
version: 4.22.1
jsonwebtoken:
specifier: ^9.0.3
version: 9.0.3
node-cron:
specifier: ^3.0.3
version: 3.0.3
openai:
specifier: ^4.104.0
version: 4.104.0(ws@8.19.0)(zod@4.3.6)
pnpm:
specifier: ^10.29.2
version: 10.30.2
pokersolver:
specifier: ^2.1.4
version: 2.1.4
@@ -1105,6 +1111,11 @@ packages:
resolution:
{ integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== }
jsonwebtoken@9.0.3:
resolution:
{ integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== }
engines: { node: ">=12", npm: ">=6" }
jwa@2.0.1:
resolution:
{ integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== }
@@ -1127,10 +1138,38 @@ packages:
{ integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== }
engines: { node: ">=10" }
lodash.includes@4.3.0:
resolution:
{ integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== }
lodash.isboolean@3.0.3:
resolution:
{ integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== }
lodash.isinteger@4.0.4:
resolution:
{ integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== }
lodash.isnumber@3.0.3:
resolution:
{ integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== }
lodash.isplainobject@4.0.6:
resolution:
{ integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== }
lodash.isstring@4.0.1:
resolution:
{ integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== }
lodash.merge@4.6.2:
resolution:
{ integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== }
lodash.once@4.1.1:
resolution:
{ integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== }
lodash.snakecase@4.1.1:
resolution:
{ integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== }
@@ -1361,6 +1400,12 @@ packages:
resolution:
{ integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig== }
pnpm@10.30.2:
resolution:
{ integrity: sha512-Ns3HB+e3lAqYjJwez4jQhPhRS1w/CF9TouJEwpIdOyVFvCDdTr4fwkX+7EY7spiuzqemPtH3aAuHfcY3nY0MtA== }
engines: { node: ">=18.12" }
hasBin: true
pokersolver@2.1.4:
resolution:
{ integrity: sha512-vmgZS+K8H8r1RePQykFM5YyvlKo1v3xVec8FMBjg9N6mR2Tj/n/X415w+lG67FWbrk71D/CADmKFinDgaQlAsw== }
@@ -2682,6 +2727,19 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.4
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
@@ -2706,8 +2764,22 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.merge@4.6.2: {}
lodash.once@4.1.1: {}
lodash.snakecase@4.1.1: {}
lodash@4.17.23: {}
@@ -2864,6 +2936,8 @@ snapshots:
exsolve: 1.0.8
pathe: 2.0.3
pnpm@10.30.2: {}
pokersolver@2.1.4: {}
prelude-ls@1.2.1: {}

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
onlyBuiltDependencies:
- "@prisma/client"
- "@prisma/engines"
- prisma
- protobufjs

57
src/api/cs.js Normal file
View File

@@ -0,0 +1,57 @@
import { csSkinsData, csSkinsPrices } from "../utils/cs.state.js";
const params = new URLSearchParams({
app_id: 730,
currency: "EUR",
});
export const fetchSuggestedPrices = async () => {
try {
const response = await fetch(`https://api.skinport.com/v1/items?${params}`, {
method: "GET",
headers: { "Accept-Encoding": "br" },
});
const data = await response.json();
data.forEach((skin) => {
if (skin.market_hash_name) {
csSkinsPrices[skin.market_hash_name] = {
suggested_price: skin.suggested_price,
min_price: skin.min_price,
max_price: skin.max_price,
mean_price: skin.mean_price,
median_price: skin.median_price,
};
}
});
return data;
} catch (error) {
console.error("Error parsing JSON:", error);
return null;
}
};
export const fetchSkinsData = async () => {
try {
const response = await fetch(
`https://raw.githubusercontent.com/ByMykel/CSGO-API/main/public/api/en/skins.json`,
);
const data = await response.json();
let rarities = {};
data.forEach((skin) => {
if (skin.market_hash_name) {
csSkinsData[skin.market_hash_name] = skin;
} else if (skin.name) {
csSkinsData[skin.name] = skin;
}
if (skin.rarity && skin.rarity.name) {
rarities[skin.rarity.name] = (rarities[skin.rarity.name] || 0) + 1;
}
});
console.log(rarities)
return data;
} catch (error) {
console.error("Error fetching skins data:", error);
return null;
}
};

View File

@@ -1,5 +1,6 @@
import { handleMessageCreate } from "./handlers/messageCreate.js";
import { getAkhys } from "../utils/index.js";
import { fetchSuggestedPrices, fetchSkinsData } from "../api/cs.js";
/**
* Initializes and attaches all necessary event listeners to the Discord client.
@@ -18,6 +19,8 @@ export function initializeEvents(client, io) {
await getAkhys(client);
console.log("[Startup] Setting up scheduled tasks...");
//setupCronJobs(client, io);
await fetchSuggestedPrices();
await fetchSkinsData();
console.log("--- FlopoBOT is fully operational ---");
});

View File

@@ -19,6 +19,9 @@ import * as skinService from "../../services/skin.service.js";
import * as logService from "../../services/log.service.js";
import { client } from "../client.js";
import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js";
import { fetchSuggestedPrices, fetchSkinsData } from "../../api/cs.js";
import { csSkinsData, csSkinsPrices } from "../../utils/cs.state.js";
import { getRandomSkinWithRandomSpecs } from "../../utils/cs.utils.js";
// Constants for the AI rate limiter
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
@@ -379,6 +382,99 @@ async function handleAdminCommands(message) {
message.reply(`Error during refund skins ${e.message}`);
}
break;
case `${prefix}:cs-search`:
try {
const searchTerm = args.join(" ");
if (!searchTerm) {
message.reply("Please provide a search term.");
return;
}
const filteredData = csSkinsData
? Object.values(csSkinsData).filter((skin) => {
const name = skin.market_hash_name.toLowerCase();
return args.every((word) => name.includes(word.toLowerCase()));
})
: [];
if (filteredData.length === 0) {
message.reply(`No skins found matching "${searchTerm}".`);
return;
} else if (filteredData.length <= 10) {
const skinList = filteredData
.map(
(skin) =>
`${skin.market_hash_name} - ${
csSkinsPrices[skin.market_hash_name]
? "Sug " +
csSkinsPrices[skin.market_hash_name].suggested_price +
" | Min " +
csSkinsPrices[skin.market_hash_name].min_price +
" | Max " +
csSkinsPrices[skin.market_hash_name].max_price +
" | Avg " +
csSkinsPrices[skin.market_hash_name].mean_price +
" | Med " +
csSkinsPrices[skin.market_hash_name].median_price
: "N/A"
}`,
)
.join("\n");
message.reply(`Skins matching "${searchTerm}":\n${skinList}`);
} else {
message.reply(`Found ${filteredData.length} skins matching "${searchTerm}".`);
}
} catch (e) {
console.log(e);
message.reply(`Error searching CS:GO skins: ${e.message}`);
}
break;
case `${prefix}:open-cs`:
try {
const randomSkin = getRandomSkinWithRandomSpecs(args[0] ? parseFloat(args[0]) : null);
message.reply(
`You opened a CS:GO case and got: ${randomSkin.name} (${randomSkin.data.rarity.name}, ${
randomSkin.isStattrak ? "StatTrak, " : ""
}${randomSkin.isSouvenir ? "Souvenir, " : ""}${randomSkin.wearState} - float ${randomSkin.float})\nBase Price: ${
randomSkin.price ?? "N/A"
} Flopos\nImage url: [url](${randomSkin.data.image || "N/A"})`,
);
} catch (e) {
console.log(e);
message.reply(`Error opening CS:GO case: ${e.message}`);
}
break;
case `${prefix}:simulate-cs`:
try {
const caseCount = parseInt(args[0]) || 100;
const caseType = args[1] || "default";
let totalResValue = 0;
let highestSkinPrice = 0;
const priceTiers = {
"Consumer Grade": 0,
"Industrial Grade": 0,
"Mil-Spec Grade": 0,
"Restricted": 0,
"Classified": 0,
"Covert": 0,
"Extraordinary": 0,
};
for (let i = 0; i < caseCount; i++) {
const result = getRandomSkinWithRandomSpecs();
totalResValue += parseInt(result.price);
if (parseInt(result.price) > highestSkinPrice) {
highestSkinPrice = parseInt(result.price);
}
priceTiers[result.data.rarity.name]++;
}
console.log(totalResValue / caseCount);
message.reply(
`${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`,
);
} catch (e) {
console.log(e);
message.reply(`Error during case simulation: ${e.message}`);
}
break;
}
}

View File

@@ -1297,10 +1297,6 @@ export function apiRoutes(client, io) {
},
});
console.log(
`[CHECKOUT] New session for user ${userId}: ${session.id}, offer: ${offer.id} (${offer.coins} coins for ${offer.amount_cents} cents)`,
);
res.json({ sessionId: session.id, url: session.url });
} catch (error) {
console.error("Error creating checkout session:", error);

View File

@@ -23,7 +23,7 @@ router.get("/discord", (req, res) => {
response_type: "code",
scope: "identify",
});
console.log("Redirecting to Discord OAuth2 with params:", params.toString());
res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`);
});
@@ -105,7 +105,7 @@ router.get("/me", async (req, res) => {
const user = await userService.getUser(payload.discordId);
if (!user) {
console.warn("User not found for Discord ID in token:", payload.discordId);
return res.json({discordId: payload.discordId});
return res.json({ discordId: payload.discordId });
}
res.json({ user, discordId: user.id });

View File

@@ -264,12 +264,15 @@ async function onSnakeGameStateUpdate(client, eventData) {
winnerId = lobby.p2.id;
}
// If scores are equal, winnerId remains null (draw)
io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId });
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
} else if (lobby.p1.win || lobby.p2.win) {
// One player won by filling the grid
const winnerId = lobby.p1.win ? lobby.p1.id : lobby.p2.id;
io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId });
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
}
delete activeSnakeGames[lobby.gameKey];
}
export async function onGameOver(client, gameType, playerId, winnerId, reason = "", scores = null) {
@@ -284,6 +287,8 @@ export async function onGameOver(client, gameType, playerId, winnerId, reason =
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase(), scores);
resultText = "Égalité";
} else {
// Temp fix: Don't update ELO for Snake since it's not in a stable state yet.
if (gameType !== "snake") {
await eloHandler(
game.p1.id,
game.p2.id,
@@ -292,6 +297,7 @@ export async function onGameOver(client, gameType, playerId, winnerId, reason =
title.toUpperCase(),
scores,
);
}
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
resultText = `Victoire de ${winnerName}`;
}

3
src/utils/cs.state.js Normal file
View File

@@ -0,0 +1,3 @@
export let csSkinsData = {};
export let csSkinsPrices = {};

128
src/utils/cs.utils.js Normal file
View File

@@ -0,0 +1,128 @@
import { csSkinsData, csSkinsPrices } from "./cs.state.js";
const StateFactoryNew = "Factory New";
const StateMinimalWear = "Minimal Wear";
const StateFieldTested = "Field-Tested";
const StateWellWorn = "Well-Worn";
const StateBattleScarred = "Battle-Scarred";
export const RarityToColor = {
Gold: 0xffd700, // Standard Gold
Covert: 0xeb4b4b, // Red
Classified: 0xd32ce6, // Pink/Magenta
Restricted: 0x8847ff, // Purple
"Mil-Spec Grade": 0x4b69ff, // Dark Blue
"Industrial Grade": 0x5e98d9, // Light Blue
"Consumer Grade": 0xb0c3d9, // Light Grey/White
};
const basePriceRanges = {
"Consumer Grade": { min: 1, max: 5 },
"Industrial Grade": { min: 2, max: 10 },
"Mil-Spec Grade": { min: 3, max: 70 },
"Restricted": { min: 17, max: 400 },
"Classified": { min: 70, max: 1700 },
"Covert": { min: 350, max: 17000 },
"Gold": { min: 10000, max: 100000 },
"Extraordinary": { min: 10000, max: 100000 },
};
const wearStateMultipliers = {
[StateFactoryNew]: 1,
[StateMinimalWear]: 0.75,
[StateFieldTested]: 0.65,
[StateWellWorn]: 0.6,
[StateBattleScarred]: 0.5,
};
export function randomSkinRarity() {
const roll = Math.random();
const goldLimit = 0.003;
const covertLimit = goldLimit + 0.014;
const classifiedLimit = covertLimit + 0.04;
const restrictedLimit = classifiedLimit + 0.2;
const milSpecLimit = restrictedLimit + 0.5;
const industrialLimit = milSpecLimit + 0.2;
if (roll < goldLimit) return "Extraordinary";
if (roll < covertLimit) return "Covert";
if (roll < classifiedLimit) return "Classified";
if (roll < restrictedLimit) return "Restricted";
if (roll < milSpecLimit) return "Mil-Spec Grade";
if (roll < industrialLimit) return "Industrial Grade";
return "Consumer Grade";
}
export function generatePrice(rarity, float, isStattrak, isSouvenir, wearState) {
const ranges = basePriceRanges[rarity] || basePriceRanges["Industrial Grade"];
console.log(ranges)
let basePrice = ranges.min + (Math.random()) * (ranges.max - ranges.min);
console.log(basePrice)
const stateMultiplier = wearStateMultipliers[wearState] ?? 1.0;
console.log(stateMultiplier)
let finalPrice = basePrice * stateMultiplier;
console.log(finalPrice)
const isExtraordinary = rarity === "Extraordinary";
if (isSouvenir && !isExtraordinary) {
finalPrice *= 4 + (Math.random()) * (10.0 - 4);
} else if (isStattrak && !isExtraordinary) {
finalPrice *= 3 + (Math.random()) * (5.0 - 3);
}
console.log(finalPrice)
finalPrice /= 1 + float; // Avoid division by zero and ensure float has a significant impact
if (finalPrice < 1) finalPrice = 1;
return finalPrice.toFixed(0);
}
export function isStattrak(canBeStattrak) {
if (!canBeStattrak) return false;
return Math.random() < 0.15;
}
export function isSouvenir(canBeSouvenir) {
if (!canBeSouvenir) return false;
return Math.random() < 0.15;
}
export function getRandomFloatInRange(min, max) {
return min + (Math.random()) * (max - min);
}
export function getWearState(wear) {
const clamped = Math.max(0.0, Math.min(1.0, wear));
if (clamped < 0.07) return StateFactoryNew;
if (clamped < 0.15) return StateMinimalWear;
if (clamped < 0.38) return StateFieldTested;
if (clamped < 0.45) return StateWellWorn;
return StateBattleScarred;
}
export function getRandomSkinWithRandomSpecs(u_float=null) {
const skinNames = Object.keys(csSkinsData);
const randomRarity = randomSkinRarity();
console.log(randomRarity)
const filteredSkinNames = skinNames.filter(name => csSkinsData[name].rarity.name === randomRarity);
const randomIndex = Math.floor(Math.random() * filteredSkinNames.length);
const skinName = filteredSkinNames[randomIndex];
const skinData = csSkinsData[skinName];
const float = u_float !== null ? u_float : getRandomFloatInRange(skinData.min_float, skinData.max_float);
return {
name: skinName,
data: skinData,
isStattrak: isStattrak(skinData.stattrak),
isSouvenir: isSouvenir(skinData.souvenir),
wearState: getWearState(float),
float: float,
price: generatePrice(skinData.rarity.name, float, isStattrak(skinData.stattrak), isSouvenir(skinData.souvenir), getWearState(float)),
};
}