clean, lint, format

This commit is contained in:
milo
2026-02-10 02:20:24 +01:00
parent 2cabd43769
commit 9e12065f0d
21 changed files with 3587 additions and 190 deletions

View File

@@ -7,9 +7,9 @@
``` ```
├── public/ ├── public/
│ └── images/ # Static assets │ └── images/ # Static assets
├── src/ ├── src/
│ ├── api/ # External API integrations │ ├── api/ # External API integrations
│ ├── bot/ │ ├── bot/
│ │ ├── commands/ # Slash command implementations │ │ ├── commands/ # Slash command implementations
│ │ ├── components/ # Discord message components │ │ ├── components/ # Discord message components
│ │ ├── handlers/ # Event handlers │ │ ├── handlers/ # Event handlers
@@ -17,10 +17,10 @@
│ │ └── events.js # Event registration │ │ └── events.js # Event registration
│ ├── config/ │ ├── config/
│ │ └── commands.js # Slash command definitions │ │ └── commands.js # Slash command definitions
│ ├── database/ │ ├── database/
│ │ └── index.js # Database connection and models │ │ └── index.js # Database connection and models
│ ├── game/ # Game logic and data │ ├── game/ # Game logic and data
│ ├── server/ │ ├── server/
│ │ ├── routes/ # Express routes │ │ ├── routes/ # Express routes
│ │ ├── app.js # Express app setup │ │ ├── app.js # Express app setup
│ │ └── socket.js # Socket.io setup │ │ └── socket.js # Socket.io setup
@@ -30,6 +30,7 @@
``` ```
## Features ## Features
- **Moderation Tools** : Includes commands for managing server members. - **Moderation Tools** : Includes commands for managing server members.
- **AI Integration** : Utilizes AI APIs for enhanced interactions. - **AI Integration** : Utilizes AI APIs for enhanced interactions.
- **Game Mechanics** : Implements game features and logic. - **Game Mechanics** : Implements game features and logic.
@@ -38,12 +39,14 @@
- **Web Integration** : Designed to work alongside a [FlopoSite](https://floposite.com) (see [FlopoSite's repo)](https://github.com/cassoule/floposite)). - **Web Integration** : Designed to work alongside a [FlopoSite](https://floposite.com) (see [FlopoSite's repo)](https://github.com/cassoule/floposite)).
## Additional Information ## Additional Information
Note that FlopoBot is a work in progress, and new features and improvements are continually being added. Contributions and feedback are welcome !
FlopoBot was orriginally created to be integrated in a specific Discord server, so adding it to other servers won't provide the full experience (for now). Note that FlopoBot is a work in progress, and new features and improvements are continually being added. Contributions and feedback are welcome !
FlopoBot was orriginally created to be integrated in a specific Discord server, so adding it to other servers won't provide the full experience (for now).
FlopoSite though is public and can be used by anyone :) FlopoSite though is public and can be used by anyone :)
## Related Links ## Related Links
- [FlopoSite Website](https://floposite.com) - [FlopoSite Website](https://floposite.com)
- [FlopoSite Repository](https://github.com/cassoule/floposite) - [FlopoSite Repository](https://github.com/cassoule/floposite)

36
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"axios": "^1.9.0", "axios": "^1.9.0",
"discord-interactions": "^4.0.0", "discord-interactions": "^4.0.0",
"discord.js": "^14.18.0", "discord.js": "^14.25.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
@@ -700,9 +700,9 @@
} }
}, },
"node_modules/@vladfrangu/async_event_emitter": { "node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.6", "version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v14.0.0",
@@ -856,13 +856,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -2070,9 +2070,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.9", "version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -2106,9 +2106,9 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.4", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -2813,9 +2813,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/magic-bytes.js": { "node_modules/magic-bytes.js": {
"version": "1.12.1", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {

View File

@@ -13,7 +13,10 @@
"register": "node commands.js", "register": "node commands.js",
"dev": "nodemon index.js", "dev": "nodemon index.js",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev" "prisma:migrate": "prisma migrate dev",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"stripe:whook": "stripe listen --forward-to localhost:3000/api/buy-coins"
}, },
"author": "Milo Gourvest", "author": "Milo Gourvest",
"license": "MIT", "license": "MIT",
@@ -23,7 +26,7 @@
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"axios": "^1.9.0", "axios": "^1.9.0",
"discord-interactions": "^4.0.0", "discord-interactions": "^4.0.0",
"discord.js": "^14.18.0", "discord.js": "^14.25.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",

3167
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
/*
Warnings:
- You are about to alter the column `offered_at` on the `bids` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `timestamp` on the `games` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `created_at` on the `logs` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `closing_at` on the `market_offers` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `opening_at` on the `market_offers` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `posted_at` on the `market_offers` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `created_at` on the `transactions` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_bids" (
"id" TEXT NOT NULL PRIMARY KEY,
"bidder_id" TEXT NOT NULL,
"market_offer_id" TEXT NOT NULL,
"offer_amount" INTEGER NOT NULL,
"offered_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "bids_bidder_id_fkey" FOREIGN KEY ("bidder_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "bids_market_offer_id_fkey" FOREIGN KEY ("market_offer_id") REFERENCES "market_offers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_bids" ("bidder_id", "id", "market_offer_id", "offer_amount", "offered_at") SELECT "bidder_id", "id", "market_offer_id", "offer_amount", "offered_at" FROM "bids";
DROP TABLE "bids";
ALTER TABLE "new_bids" RENAME TO "bids";
CREATE TABLE "new_games" (
"id" TEXT NOT NULL PRIMARY KEY,
"p1" TEXT NOT NULL,
"p2" TEXT,
"p1_score" INTEGER,
"p2_score" INTEGER,
"p1_elo" INTEGER,
"p2_elo" INTEGER,
"p1_new_elo" INTEGER,
"p2_new_elo" INTEGER,
"type" TEXT,
"timestamp" DATETIME,
CONSTRAINT "games_p1_fkey" FOREIGN KEY ("p1") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "games_p2_fkey" FOREIGN KEY ("p2") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_games" ("id", "p1", "p1_elo", "p1_new_elo", "p1_score", "p2", "p2_elo", "p2_new_elo", "p2_score", "timestamp", "type") SELECT "id", "p1", "p1_elo", "p1_new_elo", "p1_score", "p2", "p2_elo", "p2_new_elo", "p2_score", "timestamp", "type" FROM "games";
DROP TABLE "games";
ALTER TABLE "new_games" RENAME TO "games";
CREATE TABLE "new_logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"action" TEXT,
"target_user_id" TEXT,
"coins_amount" INTEGER,
"user_new_amount" INTEGER,
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "logs_target_user_id_fkey" FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_logs" ("action", "coins_amount", "created_at", "id", "target_user_id", "user_id", "user_new_amount") SELECT "action", "coins_amount", "created_at", "id", "target_user_id", "user_id", "user_new_amount" FROM "logs";
DROP TABLE "logs";
ALTER TABLE "new_logs" RENAME TO "logs";
CREATE TABLE "new_market_offers" (
"id" TEXT NOT NULL PRIMARY KEY,
"skin_uuid" TEXT NOT NULL,
"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 RESTRICT 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";
CREATE TABLE "new_transactions" (
"id" TEXT NOT NULL PRIMARY KEY,
"session_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"coins_amount" INTEGER NOT NULL,
"amount_cents" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'eur',
"customer_email" TEXT,
"customer_name" TEXT,
"payment_status" TEXT NOT NULL,
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "transactions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_transactions" ("amount_cents", "coins_amount", "created_at", "currency", "customer_email", "customer_name", "id", "payment_status", "session_id", "user_id") SELECT "amount_cents", "coins_amount", "created_at", "currency", "customer_email", "customer_name", "id", "payment_status", "session_id", "user_id" FROM "transactions";
DROP TABLE "transactions";
ALTER TABLE "new_transactions" RENAME TO "transactions";
CREATE UNIQUE INDEX "transactions_session_id_key" ON "transactions"("session_id");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@@ -102,12 +102,14 @@ export async function handleTimeoutCommand(req, res, client) {
if (remaining === 0) { if (remaining === 0) {
clearInterval(countdownInterval); clearInterval(countdownInterval);
const votersList = (await Promise.all(poll.voters const votersList = (
.map(async (voterId) => { await Promise.all(
const user = await userService.getUser(voterId); poll.voters.map(async (voterId) => {
return `- ${user?.globalName || "Utilisateur Inconnu"}`; const user = await userService.getUser(voterId);
}) return `- ${user?.globalName || "Utilisateur Inconnu"}`;
)).join("\n"); }),
)
).join("\n");
try { try {
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
@@ -143,12 +145,14 @@ export async function handleTimeoutCommand(req, res, client) {
// --- Periodic Update Logic --- // --- Periodic Update Logic ---
// Update the message every second with the new countdown // Update the message every second with the new countdown
try { try {
const votersList = (await Promise.all(poll.voters const votersList = (
.map(async (voterId) => { await Promise.all(
const user = await userService.getUser(voterId); poll.voters.map(async (voterId) => {
return `- ${user?.globalName || "Utilisateur Inconnu"}`; const user = await userService.getUser(voterId);
}) return `- ${user?.globalName || "Utilisateur Inconnu"}`;
)).join("\n"); }),
)
).join("\n");
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
method: "PATCH", method: "PATCH",

View File

@@ -75,10 +75,14 @@ export async function handlePollVote(req, res) {
io.emit("poll-update"); // Notify frontend clients of the change io.emit("poll-update"); // Notify frontend clients of the change
const votersList = (await Promise.all(poll.voters.map(async (vId) => { const votersList = (
const user = await userService.getUser(vId); await Promise.all(
return `- ${user?.globalName || "Utilisateur Inconnu"}`; poll.voters.map(async (vId) => {
}))).join("\n"); const user = await userService.getUser(vId);
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}),
)
).join("\n");
// --- 4. Check for Majority --- // --- 4. Check for Majority ---
if (isVotingFor && poll.for >= poll.requiredMajority) { if (isVotingFor && poll.for >= poll.requiredMajority) {

View File

@@ -190,22 +190,22 @@ async function handleAdminCommands(message) {
switch (command) { switch (command) {
case "?sp": case "?sp":
let msgText = "" let msgText = "";
for (let skinTierRank = 1; skinTierRank <= 4; skinTierRank++) { for (let skinTierRank = 1; skinTierRank <= 4; skinTierRank++) {
msgText += `\n--- Tier Rank: ${skinTierRank} ---\n`; msgText += `\n--- Tier Rank: ${skinTierRank} ---\n`;
let skinMaxLevels = 4; let skinMaxLevels = 4;
let skinMaxChromas = 4; let skinMaxChromas = 4;
for (let skinLevel = 1; skinLevel < skinMaxLevels; skinLevel++) { for (let skinLevel = 1; skinLevel < skinMaxLevels; skinLevel++) {
msgText += (`Levels: ${skinLevel}/${skinMaxLevels}, MaxChromas: ${1}/${skinMaxChromas} - `); msgText += `Levels: ${skinLevel}/${skinMaxLevels}, MaxChromas: ${1}/${skinMaxChromas} - `;
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `); msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `); msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`); msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
} }
for (let skinChroma = 1; skinChroma < skinMaxChromas; skinChroma++) { for (let skinChroma = 1; skinChroma < skinMaxChromas; skinChroma++) {
msgText += (`Levels: ${skinMaxLevels}/${skinMaxLevels}, MaxChromas: ${skinChroma}/${skinMaxChromas} - `); msgText += `Levels: ${skinMaxLevels}/${skinMaxLevels}, MaxChromas: ${skinChroma}/${skinMaxChromas} - `;
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `); msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `); msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`); msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
} }
message.reply(msgText); message.reply(msgText);
msgText = ""; msgText = "";

View File

@@ -97,35 +97,33 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = nu
if (scores) { if (scores) {
await gameService.insertGame({ await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`, id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id, p1: p1Id,
p2: p2Id, p2: p2Id,
p1Score: scores.p1, p1Score: scores.p1,
p2Score: scores.p2, p2Score: scores.p2,
p1Elo: p1CurrentElo, p1Elo: p1CurrentElo,
p2Elo: p2CurrentElo, p2Elo: p2CurrentElo,
p1NewElo: finalP1Elo, p1NewElo: finalP1Elo,
p2NewElo: finalP2Elo, p2NewElo: finalP2Elo,
type: type, type: type,
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
await gameService.insertGame({ await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`, id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id, p1: p1Id,
p2: p2Id, p2: p2Id,
p1Score: p1Score, p1Score: p1Score,
p2Score: p2Score, p2Score: p2Score,
p1Elo: p1CurrentElo, p1Elo: p1CurrentElo,
p2Elo: p2CurrentElo, p2Elo: p2CurrentElo,
p1NewElo: finalP1Elo, p1NewElo: finalP1Elo,
p2NewElo: finalP2Elo, p2NewElo: finalP2Elo,
type: type, type: type,
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
} }
/** /**
@@ -142,12 +140,14 @@ export async function pokerEloHandler(room) {
if (playerIds.length < 2) return; // Not enough players to calculate Elo if (playerIds.length < 2) return; // Not enough players to calculate Elo
// Fetch all players' Elo data at once // Fetch all players' Elo data at once
const dbPlayers = await Promise.all(playerIds.map(async (id) => { const dbPlayers = await Promise.all(
const user = await userService.getUser(id); playerIds.map(async (id) => {
const eloData = await gameService.getUserElo(id); const user = await userService.getUser(id);
const elo = eloData?.elo || 1000; const eloData = await gameService.getUserElo(id);
return { ...user, elo }; const elo = eloData?.elo || 1000;
})); return { ...user, elo };
}),
);
const winnerIds = new Set(room.winners); const winnerIds = new Set(room.winners);
const playerCount = dbPlayers.length; const playerCount = dbPlayers.length;

View File

@@ -157,15 +157,15 @@ export function apiRoutes(client, io) {
); );
const updatedSkin = await skinService.getSkin(result.randomSkinData.uuid); const updatedSkin = await skinService.getSkin(result.randomSkinData.uuid);
await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client); await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client);
const contentSkins = selectedSkins.map((item) => { const contentSkins = selectedSkins.map((item) => {
return { return {
...item, ...item,
isMelee: isMeleeSkin(item.displayName), isMelee: isMeleeSkin(item.displayName),
isVCT: isVCTSkin(item.displayName), isVCT: isVCTSkin(item.displayName),
isChampions: isChampionsSkin(item.displayName), isChampions: isChampionsSkin(item.displayName),
vctRegion: getVCTRegion(item.displayName), vctRegion: getVCTRegion(item.displayName),
} };
}); });
res.json({ res.json({
selectedSkins: contentSkins, selectedSkins: contentSkins,
@@ -236,9 +236,7 @@ export function apiRoutes(client, io) {
try { try {
const skin = await skinService.getSkin(req.params.uuid); const skin = await skinService.getSkin(req.params.uuid);
const skinData = skins.find((s) => s.uuid === skin.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid);
if ( if (!skinData) {
!skinData
) {
return res.status(403).json({ error: "Invalid skin." }); return res.status(403).json({ error: "Invalid skin." });
} }
if (skin.userId !== userId) { if (skin.userId !== userId) {
@@ -248,7 +246,9 @@ export function apiRoutes(client, io) {
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid); const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open"); const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open");
if (activeOffers.length > 0) { if (activeOffers.length > 0) {
return res.status(403).json({ error: "Impossible de vendre ce skin, une offre FlopoMarket est déjà en cours." }); 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); const commandUser = await userService.getUser(userId);
@@ -288,14 +288,14 @@ export function apiRoutes(client, io) {
const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData); const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData);
const segments = [ const segments = [
{ id: 'SUCCEEDED', color: '5865f2', percent: successProb, label: 'Réussie' }, { id: "SUCCEEDED", color: "5865f2", percent: successProb, label: "Réussie" },
{ id: 'DESTRUCTED', color: 'f26558', percent: destructionProb, label: 'Détruit' }, { id: "DESTRUCTED", color: "f26558", percent: destructionProb, label: "Détruit" },
{ id: 'NONE', color: '18181818', percent: 1 - successProb - destructionProb, label: 'Échec' }, { id: "NONE", color: "18181818", percent: 1 - successProb - destructionProb, label: "Échec" },
] ];
res.json({ segments, upgradePrice }); res.json({ segments, upgradePrice });
} catch (error) { } catch (error) {
console.log(error) console.log(error);
res.status(500).json({ error: "Failed to fetch skin upgrade." }); res.status(500).json({ error: "Failed to fetch skin upgrade." });
} }
}); });
@@ -305,10 +305,7 @@ export function apiRoutes(client, io) {
try { try {
const skin = await skinService.getSkin(req.params.uuid); const skin = await skinService.getSkin(req.params.uuid);
const skinData = skins.find((s) => s.uuid === skin.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid);
if ( if (!skinData || (skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length)) {
!skinData ||
(skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length)
) {
return res.status(403).json({ error: "Skin is already maxed out or invalid skin." }); return res.status(403).json({ error: "Skin is already maxed out or invalid skin." });
} }
if (skin.userId !== userId) { if (skin.userId !== userId) {
@@ -328,7 +325,7 @@ export function apiRoutes(client, io) {
if (commandUser.coins < upgradePrice) { if (commandUser.coins < upgradePrice) {
return res.status(403).json({ error: `Pas assez de FlopoCoins (${upgradePrice} requis).` }); return res.status(403).json({ error: `Pas assez de FlopoCoins (${upgradePrice} requis).` });
} }
await logService.insertLog({ await logService.insertLog({
id: `${userId}-${Date.now()}`, id: `${userId}-${Date.now()}`,
userId: userId, userId: userId,
@@ -341,7 +338,7 @@ export function apiRoutes(client, io) {
let succeeded = false; let succeeded = false;
let destructed = false; let destructed = false;
const roll = Math.random(); const roll = Math.random();
if (roll < destructionProb) { if (roll < destructionProb) {
destructed = true; destructed = true;
@@ -363,7 +360,7 @@ export function apiRoutes(client, io) {
return parseFloat(result.toFixed(0)); return parseFloat(result.toFixed(0));
}; };
skin.currentPrice = calculatePrice(); skin.currentPrice = calculatePrice();
await skinService.updateSkin({ await skinService.updateSkin({
uuid: skin.uuid, uuid: skin.uuid,
userId: skin.userId, userId: skin.userId,
@@ -380,8 +377,10 @@ export function apiRoutes(client, io) {
currentPrice: null, currentPrice: null,
}); });
} }
console.log(`${commandUser.username} attempted to upgrade skin ${skin.uuid} - ${succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "FAILED"}`); console.log(
`${commandUser.username} attempted to upgrade skin ${skin.uuid} - ${succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "FAILED"}`,
);
res.json({ wonId: succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "NONE" }); res.json({ wonId: succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "NONE" });
} catch (error) { } catch (error) {
console.error("Error fetching skin upgrade:", error); console.error("Error fetching skin upgrade:", error);
@@ -470,7 +469,7 @@ export function apiRoutes(client, io) {
try { try {
const games = await gameService.getUserGames(req.params.id); const games = await gameService.getUserGames(req.params.id);
const eloHistory = games const eloHistory = games
.filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD') .filter((g) => g.type !== "POKER_ROUND" && g.type !== "SOTD")
.filter((game) => game.p2 !== null) .filter((game) => game.p2 !== null)
.map((game) => (game.p1 === req.params.id ? game.p1NewElo : game.p2NewElo)); .map((game) => (game.p1 === req.params.id ? game.p1NewElo : game.p2NewElo));
eloHistory.splice(0, 0, 1000); eloHistory.splice(0, 0, 1000);
@@ -489,7 +488,7 @@ export function apiRoutes(client, io) {
offer.skin = await skinService.getSkin(offer.skinUuid); offer.skin = await skinService.getSkin(offer.skinUuid);
offer.seller = await userService.getUser(offer.sellerId); offer.seller = await userService.getUser(offer.sellerId);
offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null; offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null;
offer.bids = await marketService.getOfferBids(offer.id) || {}; offer.bids = (await marketService.getOfferBids(offer.id)) || {};
for (const bid of offer.bids) { for (const bid of offer.bids) {
bid.bidder = await userService.getUser(bid.bidderId); bid.bidder = await userService.getUser(bid.bidderId);
} }
@@ -509,7 +508,10 @@ export function apiRoutes(client, io) {
router.get("/user/:id/games-history", async (req, res) => { router.get("/user/:id/games-history", async (req, res) => {
try { try {
const games = (await gameService.getUserGames(req.params.id)).filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD').reverse().slice(0, 50); const games = (await gameService.getUserGames(req.params.id))
.filter((g) => g.type !== "POKER_ROUND" && g.type !== "SOTD")
.reverse()
.slice(0, 50);
res.json({ games }); res.json({ games });
} catch (err) { } catch (err) {
res.status(500).json({ error: "Failed to fetch games history." }); res.status(500).json({ error: "Failed to fetch games history." });
@@ -1160,7 +1162,7 @@ export function apiRoutes(client, io) {
try { try {
const user = await userService.getUser(discordId); const user = await userService.getUser(discordId);
if (!user) return res.status(404).json({ message: "Utilisateur introuvable" }); if (!user) return res.status(404).json({ message: "Utilisateur introuvable" });
const reward = isWin ? score * 2 : score; const reward = isWin ? score * 2 : score;
const newCoins = user.coins + reward; const newCoins = user.coins + reward;
await userService.updateUserCoins(discordId, newCoins); await userService.updateUserCoins(discordId, newCoins);
await logService.insertLog({ await logService.insertLog({
@@ -1182,20 +1184,19 @@ export function apiRoutes(client, io) {
router.post("/queue/leave", async (req, res) => { router.post("/queue/leave", async (req, res) => {
const { discordId, game, reason } = req.body; const { discordId, game, reason } = req.body;
if (game === "snake" && (reason === "beforeunload" || reason === "route-leave")) { if (game === "snake" && (reason === "beforeunload" || reason === "route-leave")) {
const lobby = Object.values(activeSnakeGames).find( const lobby = Object.values(activeSnakeGames).find(
(l) => (l.p1.id === discordId || l.p2.id === discordId) && !l.gameOver, (l) => (l.p1.id === discordId || l.p2.id === discordId) && !l.gameOver,
); );
if (!lobby) return; if (!lobby) return;
const player = lobby.p1.id === discordId ? lobby.p1 : lobby.p2; const player = lobby.p1.id === discordId ? lobby.p1 : lobby.p2;
const otherPlayer = lobby.p1.id === discordId ? lobby.p2 : lobby.p1; const otherPlayer = lobby.p1.id === discordId ? lobby.p2 : lobby.p1;
if (player.gameOver === true) return res.status(200).json({ message: "Déjà quitté" }); if (player.gameOver === true) return res.status(200).json({ message: "Déjà quitté" });
player.gameOver = true; player.gameOver = true;
otherPlayer.win = true; otherPlayer.win = true;
lobby.lastmove = Date.now(); lobby.lastmove = Date.now();
// Broadcast the updated state to both players // Broadcast the updated state to both players
await socketEmit("snakegamestate", { await socketEmit("snakegamestate", {
lobby: { lobby: {
@@ -1203,7 +1204,7 @@ export function apiRoutes(client, io) {
p2: lobby.p2, p2: lobby.p2,
}, },
}); });
// Check if game should end // Check if game should end
if (lobby.p1.gameOver && lobby.p2.gameOver) { if (lobby.p1.gameOver && lobby.p2.gameOver) {
// Both players finished - determine winner // Both players finished - determine winner
@@ -1261,11 +1262,11 @@ export function apiRoutes(client, io) {
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'], payment_method_types: ["card"],
line_items: [ line_items: [
{ {
price_data: { price_data: {
currency: 'eur', currency: "eur",
product_data: { product_data: {
name: offer.label, name: offer.label,
description: `Achat de ${offer.label} pour FlopoBot`, description: `Achat de ${offer.label} pour FlopoBot`,
@@ -1275,7 +1276,7 @@ export function apiRoutes(client, io) {
quantity: 1, quantity: 1,
}, },
], ],
mode: 'payment', mode: "payment",
success_url: `${FLAPI_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`, success_url: `${FLAPI_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${FLAPI_URL}/dashboard`, cancel_url: `${FLAPI_URL}/dashboard`,
metadata: { metadata: {
@@ -1284,9 +1285,11 @@ 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)`); 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 }); res.json({ sessionId: session.id, url: session.url });
} catch (error) { } catch (error) {
console.error("Error creating checkout session:", error); console.error("Error creating checkout session:", error);
res.status(500).json({ error: "Failed to create checkout session" }); res.status(500).json({ error: "Failed to create checkout session" });
@@ -1294,7 +1297,7 @@ export function apiRoutes(client, io) {
}); });
router.post("/buy-coins", async (req, res) => { router.post("/buy-coins", async (req, res) => {
const sig = req.headers['stripe-signature']; const sig = req.headers["stripe-signature"];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) { if (!endpointSecret) {
@@ -1303,7 +1306,7 @@ export function apiRoutes(client, io) {
} }
let event; let event;
try { try {
// Verify webhook signature - requires raw body // Verify webhook signature - requires raw body
// Note: You need to configure Express to preserve raw body for this route // Note: You need to configure Express to preserve raw body for this route
@@ -1315,9 +1318,9 @@ export function apiRoutes(client, io) {
} }
// Handle the event // Handle the event
if (event.type === 'checkout.session.completed') { if (event.type === "checkout.session.completed") {
const session = event.data.object; const session = event.data.object;
// Extract metadata from the checkout session // Extract metadata from the checkout session
const commandUserId = session.metadata?.userId; const commandUserId = session.metadata?.userId;
const expectedCoins = parseInt(session.metadata?.coins); const expectedCoins = parseInt(session.metadata?.coins);
@@ -1325,7 +1328,7 @@ export function apiRoutes(client, io) {
const currency = session.currency; const currency = session.currency;
const customerEmail = session.customer_details?.email; const customerEmail = session.customer_details?.email;
const customerName = session.customer_details?.name; const customerName = session.customer_details?.name;
// Validate metadata exists // Validate metadata exists
if (!commandUserId || !expectedCoins) { if (!commandUserId || !expectedCoins) {
console.error("Missing userId or coins in session metadata"); console.error("Missing userId or coins in session metadata");
@@ -1333,7 +1336,7 @@ export function apiRoutes(client, io) {
} }
// Verify payment was successful // Verify payment was successful
if (session.payment_status !== 'paid') { if (session.payment_status !== "paid") {
console.error(`Payment not completed for session ${session.id}`); console.error(`Payment not completed for session ${session.id}`);
return res.status(400).json({ error: "Payment not completed" }); return res.status(400).json({ error: "Payment not completed" });
} }
@@ -1380,12 +1383,16 @@ export function apiRoutes(client, io) {
userNewAmount: newCoins, userNewAmount: newCoins,
}); });
console.log(`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid/100} ${currency}`); console.log(
`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid / 100} ${currency}`,
);
// Notify user via Discord if possible // Notify user via Discord if possible
try { try {
const discordUser = await client.users.fetch(commandUserId); const discordUser = await client.users.fetch(commandUserId);
await discordUser.send(`✅ Votre achat de ${expectedCoins} FlopoCoins a été confirmé ! Merci pour votre soutien !`); await discordUser.send(
`✅ Votre achat de ${expectedCoins} FlopoCoins a été confirmé ! Merci pour votre soutien !`,
);
} catch (e) { } catch (e) {
console.log(`Could not DM user ${commandUserId}:`, e.message); console.log(`Could not DM user ${commandUserId}:`, e.message);
} }

View File

@@ -183,7 +183,11 @@ export function blackjackRoutes(io) {
} }
emitUpdate("player-joined", snapshot(room)); emitUpdate("player-joined", snapshot(room));
emitPlayerUpdate({ id: userId, msg: `${user?.globalName || user?.username} a rejoint la table de Blackjack.`, timestamp: Date.now() }); emitPlayerUpdate({
id: userId,
msg: `${user?.globalName || user?.username} a rejoint la table de Blackjack.`,
timestamp: Date.now(),
});
return res.status(200).json({ message: "joined" }); return res.status(200).json({ message: "joined" });
}); });
@@ -225,7 +229,11 @@ export function blackjackRoutes(io) {
delete room.players[userId]; delete room.players[userId];
emitUpdate("player-left", snapshot(room)); emitUpdate("player-left", snapshot(room));
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
emitPlayerUpdate({ id: userId, msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`, timestamp: Date.now() }); emitPlayerUpdate({
id: userId,
msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`,
timestamp: Date.now(),
});
return res.status(200).json({ message: "left" }); return res.status(200).json({ message: "left" });
} }
}); });
@@ -361,7 +369,11 @@ export function blackjackRoutes(io) {
for (const userId of Object.keys(room.leavingAfterRound)) { for (const userId of Object.keys(room.leavingAfterRound)) {
delete room.players[userId]; delete room.players[userId];
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
emitPlayerUpdate({ id: userId, msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`, timestamp: Date.now() }); emitPlayerUpdate({
id: userId,
msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`,
timestamp: Date.now(),
});
} }
// Prepare next round // Prepare next round
startBetting(room, now); startBetting(room, now);

View File

@@ -54,7 +54,9 @@ export function monkeRoutes(client, io) {
return res.status(500).json({ error: "Failed to update user coins" }); return res.status(500).json({ error: "Failed to update user coins" });
} }
monkePaths[userId] = [{ round: 0, choice: null, result: null, bet: initialBet, extractValue: null, timestamp: Date.now() }]; monkePaths[userId] = [
{ round: 0, choice: null, result: null, bet: initialBet, extractValue: null, timestamp: Date.now() },
];
return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] }); return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] });
}); });
@@ -70,7 +72,8 @@ export function monkeRoutes(client, io) {
const currentRound = monkePaths[userId].length - 1; const currentRound = monkePaths[userId].length - 1;
if (step !== currentRound) return res.status(400).json({ error: "Invalid step for the current round" }); if (step !== currentRound) return res.status(400).json({ error: "Invalid step for the current round" });
if (monkePaths[userId][currentRound].choice !== null) return res.status(400).json({ error: "This round has already been played" }); if (monkePaths[userId][currentRound].choice !== null)
return res.status(400).json({ error: "This round has already been played" });
const randomLoseChoice = Math.floor(Math.random() * 3); // 0, 1, or 2 const randomLoseChoice = Math.floor(Math.random() * 3); // 0, 1, or 2
if (choice !== randomLoseChoice) { if (choice !== randomLoseChoice) {
@@ -79,7 +82,14 @@ export function monkeRoutes(client, io) {
monkePaths[userId][currentRound].extractValue = Math.round(monkePaths[userId][currentRound].bet * 1.33); monkePaths[userId][currentRound].extractValue = Math.round(monkePaths[userId][currentRound].bet * 1.33);
monkePaths[userId][currentRound].timestamp = Date.now(); monkePaths[userId][currentRound].timestamp = Date.now();
monkePaths[userId].push({ round: currentRound + 1, choice: null, result: null, bet: monkePaths[userId][currentRound].extractValue, extractValue: null, timestamp: Date.now() }); monkePaths[userId].push({
round: currentRound + 1,
choice: null,
result: null,
bet: monkePaths[userId][currentRound].extractValue,
extractValue: null,
timestamp: Date.now(),
});
return res.status(200).json({ message: "Round won", userGamePath: monkePaths[userId], lost: false }); return res.status(200).json({ message: "Round won", userGamePath: monkePaths[userId], lost: false });
} else { } else {

View File

@@ -132,7 +132,10 @@ export function pokerRoutes(client, io) {
if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) { if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) {
return res.status(403).json({ message: "You are already in a room or queue." }); return res.status(403).json({ message: "You are already in a room or queue." });
} }
if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > ((await userService.getUser(userId))?.coins ?? 0)) { if (
!pokerRooms[roomId].fakeMoney &&
pokerRooms[roomId].minBet > ((await userService.getUser(userId))?.coins ?? 0)
) {
return res.status(403).json({ message: "You do not have enough coins to join this room." }); return res.status(403).json({ message: "You do not have enough coins to join this room." });
} }

View File

@@ -95,7 +95,7 @@ async function onQueueJoin(client, gameType, playerId) {
console.log(`[${title}] Player ${playerId} already in queue, ignoring duplicate join.`); console.log(`[${title}] Player ${playerId} already in queue, ignoring duplicate join.`);
return; return;
} }
if (Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)) { if (Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)) {
console.log(`[${title}] Player ${playerId} already in active game, ignoring queue join.`); console.log(`[${title}] Player ${playerId} already in active game, ignoring queue join.`);
return; return;
@@ -277,7 +277,7 @@ export async function onGameOver(client, gameType, playerId, winnerId, reason =
game.p1.id === winnerId ? 1 : 0, game.p1.id === winnerId ? 1 : 0,
game.p2.id === winnerId ? 1 : 0, game.p2.id === winnerId ? 1 : 0,
title.toUpperCase(), title.toUpperCase(),
scores scores,
); );
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name; const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
resultText = `Victoire de ${winnerName}`; resultText = `Victoire de ${winnerName}`;
@@ -382,7 +382,7 @@ async function createGame(client, gameType) {
} }
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) }); io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
// For Snake, also emit a specific match notification to the two players // For Snake, also emit a specific match notification to the two players
if (gameType === "snake") { if (gameType === "snake") {
io.emit("snakematch", { io.emit("snakematch", {
@@ -395,7 +395,7 @@ async function createGame(client, gameType) {
}, },
}); });
} }
await emitQueueUpdate(client, gameType); await emitQueueUpdate(client, gameType);
} }

View File

@@ -17,9 +17,7 @@ export async function getUsersByElo() {
include: { elo: true }, include: { elo: true },
orderBy: { elo: { elo: "desc" } }, orderBy: { elo: { elo: "desc" } },
}); });
return users return users.filter((u) => u.elo).map((u) => ({ ...u, elo: u.elo?.elo ?? null }));
.filter((u) => u.elo)
.map((u) => ({ ...u, elo: u.elo?.elo ?? null }));
} }
function toGame(game) { function toGame(game) {

View File

@@ -40,15 +40,17 @@ export async function getMarketOffersBySkin(skinUuid) {
buyer: { select: { username: true, globalName: true } }, buyer: { select: { username: true, globalName: true } },
}, },
}); });
return offers.map((offer) => toOffer({ return offers.map((offer) =>
...offer, toOffer({
skinName: offer.skin?.displayName, ...offer,
skinIcon: offer.skin?.displayIcon, skinName: offer.skin?.displayName,
sellerName: offer.seller?.username, skinIcon: offer.skin?.displayIcon,
sellerGlobalName: offer.seller?.globalName, sellerName: offer.seller?.username,
buyerName: offer.buyer?.username ?? null, sellerGlobalName: offer.seller?.globalName,
buyerGlobalName: offer.buyer?.globalName ?? null, buyerName: offer.buyer?.username ?? null,
})); buyerGlobalName: offer.buyer?.globalName ?? null,
}),
);
} }
export async function insertMarketOffer(data) { export async function insertMarketOffer(data) {

View File

@@ -14,12 +14,11 @@ export async function deleteSOTD() {
export async function getAllSOTDStats() { export async function getAllSOTDStats() {
const stats = await prisma.sotdStat.findMany({ const stats = await prisma.sotdStat.findMany({
include: { user: { select: { globalName: true } } }, include: { user: { select: { globalName: true, avatarUrl: true } } },
orderBy: [{ score: "desc" }, { moves: "asc" }, { time: "asc" }], orderBy: [{ score: "desc" }, { moves: "asc" }, { time: "asc" }],
}); });
return stats.map((s) => ({ return stats.map((s) => ({
...s, ...s,
globalName: s.user?.globalName,
})); }));
} }

View File

@@ -1,4 +1,5 @@
import prisma from "../prisma/client.js"; import prisma from "../prisma/client.js";
import { socketEmit } from "../server/socket.js";
export async function getUser(id) { export async function getUser(id) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@@ -36,6 +37,7 @@ export async function updateUser(data) {
} }
export async function updateUserCoins(id, coins) { export async function updateUserCoins(id, coins) {
await socketEmit("data-updated", { table: "users", action: "update", userId: id, newCoins: coins });
return prisma.user.update({ where: { id }, data: { coins } }); return prisma.user.update({ where: { id }, data: { coins } });
} }

View File

@@ -5,10 +5,12 @@ import { isChampionsSkin } from "./index.js";
export async function drawCaseContent(caseType = "standard", poolSize = 100) { export async function drawCaseContent(caseType = "standard", poolSize = 100) {
if (caseType === "esport") { if (caseType === "esport") {
// Esport case: return all esport skins // Esport case: return all esport skins
try { try {
const dbSkins = await skinService.getAllAvailableSkins(); const dbSkins = await skinService.getAllAvailableSkins();
const esportSkins = []; const esportSkins = [];
for (const s of skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid))) { for (const s of skins.filter((s) =>
dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid),
)) {
const dbSkin = await skinService.getSkin(s.uuid); const dbSkin = await skinService.getSkin(s.uuid);
esportSkins.push({ esportSkins.push({
...s, ...s,
@@ -59,14 +61,14 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)) .filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid))
.filter((s) => { .filter((s) => {
if (caseType === "ultra") { if (caseType === "ultra") {
return !(s.displayName.toLowerCase().includes("vct") && s.displayName.toLowerCase().includes("classic")) return !(s.displayName.toLowerCase().includes("vct") && s.displayName.toLowerCase().includes("classic"));
} else { } else {
return !s.displayName.toLowerCase().includes("vct"); return !s.displayName.toLowerCase().includes("vct");
} }
}) })
.filter((s) => { .filter((s) => {
if (caseType === "ultra") { if (caseType === "ultra") {
return true return true;
} else { } else {
return isChampionsSkin(s.displayName) === false; return isChampionsSkin(s.displayName) === false;
} }
@@ -75,7 +77,8 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
for (const s of filtered) { for (const s of filtered) {
const dbSkin = await skinService.getSkin(s.uuid); const dbSkin = await skinService.getSkin(s.uuid);
const weight = tierWeights[s.contentTierUuid] ?? 0; const weight = tierWeights[s.contentTierUuid] ?? 0;
if (weight > 0) { // <--- CRITICAL: Remove 0 weight skins if (weight > 0) {
// <--- CRITICAL: Remove 0 weight skins
weightedPool.push({ weightedPool.push({
...s, ...s,
tierColor: dbSkin?.tierColor, tierColor: dbSkin?.tierColor,
@@ -90,7 +93,7 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
const result = []; const result = [];
// 2. Adjust count if the pool is smaller than requested // 2. Adjust count if the pool is smaller than requested
const actualCount = Math.min(count, list.length) ; const actualCount = Math.min(count, list.length);
for (let i = 0; i < actualCount; i++) { for (let i = 0; i < actualCount; i++) {
let r = Math.random() * totalWeight; let r = Math.random() * totalWeight;
@@ -172,10 +175,19 @@ export async function drawCaseSkin(caseContent) {
export function getSkinUpgradeProbs(skin, skinData) { export function getSkinUpgradeProbs(skin, skinData) {
const successProb = const successProb =
(1 - (((skin.currentChroma + skin.currentLvl + skinData.chromas.length + skinData.levels.length) / 18) * (parseInt(skin.tierRank) / 4)))/1.5; (1 -
const destructionProb = ((skin.currentChroma + skinData.levels.length) / (skinData.chromas.length + skinData.levels.length)) * (parseInt(skin.tierRank) / 5) * 0.075; ((skin.currentChroma + skin.currentLvl + skinData.chromas.length + skinData.levels.length) / 18) *
(parseInt(skin.tierRank) / 4)) /
1.5;
const destructionProb =
((skin.currentChroma + skinData.levels.length) / (skinData.chromas.length + skinData.levels.length)) *
(parseInt(skin.tierRank) / 5) *
0.075;
const nextLvl = skin.currentLvl < skinData.levels.length ? skin.currentLvl + 1 : skin.currentLvl; const nextLvl = skin.currentLvl < skinData.levels.length ? skin.currentLvl + 1 : skin.currentLvl;
const nextChroma = skin.currentLvl === skinData.levels.length && skin.currentChroma < skinData.chromas.length ? skin.currentChroma + 1 : skin.currentChroma; const nextChroma =
skin.currentLvl === skinData.levels.length && skin.currentChroma < skinData.chromas.length
? skin.currentChroma + 1
: skin.currentChroma;
const calculateNextPrice = () => { const calculateNextPrice = () => {
let result = parseFloat(skin.basePrice); let result = parseFloat(skin.basePrice);
result *= 1 + nextLvl / Math.max(skinData.levels.length, 2); result *= 1 + nextLvl / Math.max(skinData.levels.length, 2);
@@ -187,10 +199,18 @@ export function getSkinUpgradeProbs(skin, skinData) {
return { successProb, destructionProb, upgradePrice }; return { successProb, destructionProb, upgradePrice };
} }
export function getDummySkinUpgradeProbs(skinLevel, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, skinMaxPrice) { export function getDummySkinUpgradeProbs(
skinLevel,
skinChroma,
skinTierRank,
skinMaxLevels,
skinMaxChromas,
skinMaxPrice,
) {
const successProb = const successProb =
1 - (((skinChroma + skinLevel + (skinMaxChromas + skinMaxLevels)) / 18) * (parseInt(skinTierRank) / 4)); 1 - ((skinChroma + skinLevel + (skinMaxChromas + skinMaxLevels)) / 18) * (parseInt(skinTierRank) / 4);
const destructionProb = ((skinChroma + skinMaxLevels) / (skinMaxChromas + skinMaxLevels)) * (parseInt(skinTierRank) / 5) * 0.1; const destructionProb =
const upgradePrice = Math.max(Math.floor((parseFloat(skinMaxPrice) * (1 - successProb))), 1); ((skinChroma + skinMaxLevels) / (skinMaxChromas + skinMaxLevels)) * (parseInt(skinTierRank) / 5) * 0.1;
const upgradePrice = Math.max(Math.floor(parseFloat(skinMaxPrice) * (1 - successProb)), 1);
return { successProb, destructionProb, upgradePrice }; return { successProb, destructionProb, upgradePrice };
} }

View File

@@ -432,10 +432,26 @@ function formatTierText(rank, displayName) {
export function isMeleeSkin(skinName) { export function isMeleeSkin(skinName) {
const name = skinName.toLowerCase(); const name = skinName.toLowerCase();
return !(name.includes("classic") || name.includes("shorty") || name.includes("frenzy") || name.includes("ghost") || name.includes("sheriff") || name.includes("stinger") || name.includes("spectre") || return !(
name.includes("bucky") || name.includes("judge") || name.includes("bulldog") || name.includes("guardian") || name.includes("classic") ||
name.includes("vandal") || name.includes("phantom") || name.includes("marshal") || name.includes("outlaw") || name.includes("shorty") ||
name.includes("operator") || name.includes("ares") || name.includes("odin")); name.includes("frenzy") ||
name.includes("ghost") ||
name.includes("sheriff") ||
name.includes("stinger") ||
name.includes("spectre") ||
name.includes("bucky") ||
name.includes("judge") ||
name.includes("bulldog") ||
name.includes("guardian") ||
name.includes("vandal") ||
name.includes("phantom") ||
name.includes("marshal") ||
name.includes("outlaw") ||
name.includes("operator") ||
name.includes("ares") ||
name.includes("odin")
);
} }
export function isVCTSkin(skinName) { export function isVCTSkin(skinName) {
@@ -444,31 +460,78 @@ export function isVCTSkin(skinName) {
} }
const VCT_TEAMS = { const VCT_TEAMS = {
"vct-am": [ "vct-am": [
/x 100t\)$/g, /x c9\)$/g, /x eg\)$/g, /x fur\)$/g, /x krü\)$/g, /x lev\)$/g, /x loud\)$/g, /x 100t\)$/g,
/x mibr\)$/g, /x sen\)$/g, /x nrg\)$/g, /x g2\)$/g, /x nv\)$/g, /x 2g\)$/g /x c9\)$/g,
], /x eg\)$/g,
"vct-emea": [ /x fur\)$/g,
/x bbl\)$/g, /x fnc\)$/g, /x fut\)$/g, /x m8\)$/g, /x gx\)$/g, /x kc\)$/g, /x navi\)$/g, /x k\)$/g,
/x th\)$/g, /x tl\)$/g, /x vit\)$/g, /x ulf\)$/g, /x pcf\)$/g, /x koi\)$/g, /x apk\)$/g /x lev\)$/g,
], /x loud\)$/g,
"vct-pcf": [ /x mibr\)$/g,
/x dfm\)$/g, /x drx\)$/g, /x fs\)$/g, /x gen\)$/g, /x ge\)$/g, /x prx\)$/g, /x rrq\)$/g, /x sen\)$/g,
/x t1\)$/g, /x ts\)$/g, /x zeta\)$/g, /x vl\)$/g, /x ns\)$/g, /x tln\)$/g, /x boom\)$/g, /x bld\)$/g /x nrg\)$/g,
], /x g2\)$/g,
"vct-cn": [ /x nv\)$/g,
/x ag\)$/g, /x blg\)$/g, /x edg\)$/g, /x fpx\)$/g, /x jdg\)$/g, /x nova\)$/g, /x tec\)$/g, /x 2g\)$/g,
/x te\)$/g, /x tyl\)$/g, /x wol\)$/g, /x xlg\)$/g, /x xlg\)$/g, /x drg\)$/g ],
] "vct-emea": [
/x bbl\)$/g,
/x fnc\)$/g,
/x fut\)$/g,
/x m8\)$/g,
/x gx\)$/g,
/x kc\)$/g,
/x navi\)$/g,
/x th\)$/g,
/x tl\)$/g,
/x vit\)$/g,
/x ulf\)$/g,
/x pcf\)$/g,
/x koi\)$/g,
/x apk\)$/g,
],
"vct-pcf": [
/x dfm\)$/g,
/x drx\)$/g,
/x fs\)$/g,
/x gen\)$/g,
/x ge\)$/g,
/x prx\)$/g,
/x rrq\)$/g,
/x t1\)$/g,
/x ts\)$/g,
/x zeta\)$/g,
/x vl\)$/g,
/x ns\)$/g,
/x tln\)$/g,
/x boom\)$/g,
/x bld\)$/g,
],
"vct-cn": [
/x ag\)$/g,
/x blg\)$/g,
/x edg\)$/g,
/x fpx\)$/g,
/x jdg\)$/g,
/x nova\)$/g,
/x tec\)$/g,
/x te\)$/g,
/x tyl\)$/g,
/x wol\)$/g,
/x xlg\)$/g,
/x xlg\)$/g,
/x drg\)$/g,
],
}; };
export function getVCTRegion(skinName) { export function getVCTRegion(skinName) {
if (!isVCTSkin(skinName)) return null; if (!isVCTSkin(skinName)) return null;
const name = skinName.toLowerCase().trim(); const name = skinName.toLowerCase().trim();
for (const [region, regexes] of Object.entries(VCT_TEAMS)) { for (const [region, regexes] of Object.entries(VCT_TEAMS)) {
if (regexes.some(regex => regex.test(name))) { if (regexes.some((regex) => regex.test(name))) {
return region; return region;
} }
} }
return null; return null;
} }
@@ -476,4 +539,4 @@ export function getVCTRegion(skinName) {
export function isChampionsSkin(skinName) { export function isChampionsSkin(skinName) {
const name = skinName.toLowerCase(); const name = skinName.toLowerCase();
return name.includes("champions"); return name.includes("champions");
} }