fix: big fix + prettier

This commit is contained in:
Milo
2025-11-06 02:48:36 +01:00
parent 4025b98a86
commit b60db69157
50 changed files with 11102 additions and 8367 deletions

61
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,61 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,6 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="EditorConfigDeprecatedDescriptor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

7
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"printWidth": 120,
"useTabs": true
}

View File

@@ -1,6 +1,7 @@
# FLOPOBOT DEUXIEME DU NOM
## Project structure
Below is a basic overview of the project structure:
```
@@ -15,5 +16,6 @@ Below is a basic overview of the project structure:
```
## FlopoSite
- FlopoBot has its own website to use it a different way
[FlopoSite's repo](https://github.com/cassoule/floposite)

View File

@@ -1,5 +1,5 @@
import { registerCommands } from './src/config/commands.js';
import { registerCommands } from "./src/config/commands.js";
console.log('Registering global commands...');
console.log("Registering global commands...");
registerCommands();
console.log('Commands registered.');
console.log("Commands registered.");

9
eslint.config.js Normal file
View File

@@ -0,0 +1,9 @@
import globals from "globals";
import json from "@eslint/json";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.node } },
{ files: ["**/*.json"], plugins: { json }, language: "json/json" },
{ files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc" },
]);

View File

@@ -1,53 +1,51 @@
import 'dotenv/config';
import http from 'http';
import { Server } from 'socket.io';
import "dotenv/config";
import http from "http";
import { Server } from "socket.io";
import { app } from './src/server/app.js';
import { client } from './src/bot/client.js';
import { initializeEvents } from './src/bot/events.js';
import { initializeSocket } from './src/server/socket.js';
import { getAkhys, setupCronJobs } from './src/utils/index.js';
import { app } from "./src/server/app.js";
import { client } from "./src/bot/client.js";
import { initializeEvents } from "./src/bot/events.js";
import { initializeSocket } from "./src/server/socket.js";
import { setupCronJobs } from "./src/utils/index.js";
// --- SERVER INITIALIZATION ---
const PORT = process.env.PORT || 25578;
const server = http.createServer(app);
// --- SOCKET.IO INITIALIZATION ---
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;
export const io = new Server(server, {
cors: {
origin: FLAPI_URL,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
methods: ["GET", "POST", "PUT", "OPTIONS"],
},
pingInterval: 5000,
pingTimeout: 5000,
});
initializeSocket(io, client);
// --- BOT INITIALIZATION ---
initializeEvents(client, io);
client.login(process.env.BOT_TOKEN).then(() => {
console.log(`Logged in as ${client.user.tag}`);
console.log('[Discord Bot Events Initialized]');
console.log("[Discord Bot Events Initialized]");
});
// --- APP STARTUP ---
server.listen(PORT, async () => {
console.log(`Express+Socket.IO server listening on port ${PORT}`);
console.log(`[Connected with ${FLAPI_URL}]`);
// Initial data fetch and setup
try {
/*try {
await getAkhys(client);
} catch (error) {
console.log('Initial Fetch Error');
}
}*/
// Setup scheduled tasks
//setupCronJobs(client, io);
console.log('[Cron Jobs Initialized]');
setupCronJobs(client, io);
console.log("[Cron Jobs Initialized]");
console.log('--- FlopoBOT is ready ---');
console.log("--- FlopoBOT is ready ---");
});

1116
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,10 @@
"uuid": "^11.1.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
"@eslint/json": "^0.14.0",
"eslint": "^9.39.1",
"globals": "^16.5.0",
"nodemon": "^3.0.0",
"prettier": "3.6.2"
}
}

View File

@@ -1,11 +1,5 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard",
":preserveSemverRanges"
],
"ignorePaths": [
"**/node_modules/**"
]
"extends": ["config:recommended", ":disableDependencyDashboard", ":preserveSemverRanges"],
"ignorePaths": ["**/node_modules/**"]
}

View File

@@ -1,4 +1,4 @@
import 'dotenv/config';
import "dotenv/config";
/**
* A generic function for making requests to the Discord API.
@@ -11,7 +11,7 @@ import 'dotenv/config';
*/
export async function DiscordRequest(endpoint, options) {
// Construct the full API URL
const url = 'https://discord.com/api/v10/' + endpoint;
const url = "https://discord.com/api/v10/" + endpoint;
// Stringify the payload if it exists
if (options && options.body) {
@@ -21,16 +21,16 @@ export async function DiscordRequest(endpoint, options) {
// Use fetch to make the request, automatically including required headers
const res = await fetch(url, {
headers: {
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`,
'Content-Type': 'application/json; charset=UTF-8',
'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)',
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
"Content-Type": "application/json; charset=UTF-8",
"User-Agent": "DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)",
},
...options, // Spread the given options (e.g., method, body)
});
// If the request was not successful, throw a detailed error
if (!res.ok) {
let data
let data;
try {
data = await res.json();
} catch (err) {
@@ -54,12 +54,12 @@ export async function InstallGlobalCommands(appId, commands) {
// API endpoint for bulk overwriting global commands
const endpoint = `applications/${appId}/commands`;
console.log('Installing global commands...');
console.log("Installing global commands...");
try {
// This uses the generic DiscordRequest function to make the API call
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
console.log('Successfully installed global commands.');
await DiscordRequest(endpoint, { method: "PUT", body: commands });
console.log("Successfully installed global commands.");
} catch (err) {
console.error('Error installing global commands:', err);
console.error("Error installing global commands:", err);
}
}

View File

@@ -1,11 +1,11 @@
export async function getValorantSkins(locale='fr-FR') {
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' });
export async function getValorantSkins(locale = "fr-FR") {
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: "GET" });
const data = await response.json();
return data.data
return data.data;
}
export async function getSkinTiers(locale='fr-FR') {
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'});
export async function getSkinTiers(locale = "fr-FR") {
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: "GET" });
const data = await response.json();
return data.data
return data.data;
}

View File

@@ -1,4 +1,4 @@
import { Client, GatewayIntentBits } from 'discord.js';
import { Client, GatewayIntentBits } from "discord.js";
/**
* The single, shared Discord.js Client instance for the entire application.

View File

@@ -1,8 +1,4 @@
import {
InteractionResponseType,
MessageComponentTypes,
ButtonStyleTypes,
} from 'discord-interactions';
import { InteractionResponseType, MessageComponentTypes, ButtonStyleTypes } from "discord-interactions";
/**
* Handles the /floposite slash command.
@@ -12,7 +8,7 @@ import {
*/
export async function handleFlopoSiteCommand(req, res) {
// The URL for the link button. Consider moving to .env if it changes.
const siteUrl = process.env.FLOPOSITE_URL || 'https://floposite.com';
const siteUrl = process.env.FLOPOSITE_URL || "https://floposite.com";
// The URL for the thumbnail image.
const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`;
@@ -24,7 +20,7 @@ export async function handleFlopoSiteCommand(req, res) {
components: [
{
type: MessageComponentTypes.BUTTON,
label: 'Aller sur FlopoSite',
label: "Aller sur FlopoSite",
style: ButtonStyleTypes.LINK,
url: siteUrl,
},
@@ -35,9 +31,9 @@ export async function handleFlopoSiteCommand(req, res) {
// Define the embed message
const embeds = [
{
title: 'FlopoSite',
title: "FlopoSite",
description: "L'officiel et très goatesque site de FlopoBot.",
color: 0x6571F3, // A custom blue color
color: 0x6571f3, // A custom blue color
thumbnail: {
url: thumbnailUrl,
},

View File

@@ -1,4 +1,4 @@
import { InteractionResponseType } from 'discord-interactions';
import { InteractionResponseType } from "discord-interactions";
/**
* Handles the /info slash command.
@@ -20,9 +20,7 @@ export async function handleInfoCommand(req, res, client) {
// Filter the cached members to find those who are timed out
// A member is timed out if their `communicationDisabledUntil` property is a future date.
const timedOutMembers = guild.members.cache.filter(
(member) =>
member.communicationDisabledUntilTimestamp &&
member.communicationDisabledUntilTimestamp > Date.now()
(member) => member.communicationDisabledUntilTimestamp && member.communicationDisabledUntilTimestamp > Date.now(),
);
// --- Case 1: No members are timed out ---
@@ -32,9 +30,9 @@ export async function handleInfoCommand(req, res, client) {
data: {
embeds: [
{
title: 'Membres Timeout',
title: "Membres Timeout",
description: "Aucun membre n'est actuellement timeout.",
color: 0x4F545C, // Discord's gray color
color: 0x4f545c, // Discord's gray color
},
],
},
@@ -46,26 +44,25 @@ export async function handleInfoCommand(req, res, client) {
const memberList = timedOutMembers
.map((member) => {
// toLocaleString provides a user-friendly date and time format
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString('fr-FR');
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString("fr-FR");
return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`;
})
.join('\n');
.join("\n");
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: 'Membres Actuellement Timeout',
title: "Membres Actuellement Timeout",
description: memberList,
color: 0xED4245, // Discord's red color
color: 0xed4245, // Discord's red color
},
],
},
});
} catch (error) {
console.error('Error handling /info command:', error);
return res.status(500).json({ error: 'Failed to fetch timeout information.' });
console.error("Error handling /info command:", error);
return res.status(500).json({ error: "Failed to fetch timeout information." });
}
}

View File

@@ -3,9 +3,9 @@ import {
MessageComponentTypes,
ButtonStyleTypes,
InteractionResponseFlags,
} from 'discord-interactions';
import { activeInventories, skins } from '../../game/state.js';
import { getUserInventory } from '../../database/index.js';
} from "discord-interactions";
import { activeInventories, skins } from "../../game/state.js";
import { getUserInventory } from "../../database/index.js";
/**
* Handles the /inventory slash command.
@@ -33,11 +33,13 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [{
embeds: [
{
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
description: "Cet inventaire est vide.",
color: 0x4F545C, // Discord Gray
}],
color: 0x4f545c, // Discord Gray
},
],
},
});
}
@@ -66,18 +68,21 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
const getChromaText = (skin, skinInfo) => {
let result = "";
for (let i = 1; i <= skinInfo.chromas.length; i++) {
result += skin.currentChroma === i ? '💠 ' : '';
result += skin.currentChroma === i ? "💠 " : "";
}
return result || 'N/A';
return result || "N/A";
};
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 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';
return "Base";
};
const getImageUrl = (skin, skinInfo) => {
@@ -91,11 +96,22 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
// --- 5. Build Initial Components (Buttons) ---
const components = [
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
{
type: MessageComponentTypes.BUTTON,
custom_id: `prev_page_${interactionId}`,
label: "⏮️ Préc.",
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `next_page_${interactionId}`,
label: "Suiv. ⏭️",
style: ButtonStyleTypes.SECONDARY,
},
];
const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
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({
@@ -110,29 +126,40 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [{
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: [{
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) },
}],
components: [{ type: MessageComponentTypes.ACTION_ROW, components: components },
{ type: MessageComponentTypes.ACTION_ROW,
components: [{
},
],
components: [
{ type: MessageComponentTypes.ACTION_ROW, components: components },
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
label: 'Voir sur FlopoSite',
style: ButtonStyleTypes.LINK,}]
}],
label: "Voir sur FlopoSite",
style: ButtonStyleTypes.LINK,
},
],
},
],
},
});
} catch (error) {
console.error('Error handling /inventory command:', error);
return res.status(500).json({ error: 'Failed to generate inventory.' });
console.error("Error handling /inventory command:", error);
return res.status(500).json({ error: "Failed to generate inventory." });
}
}

View File

@@ -3,9 +3,9 @@ import {
InteractionResponseFlags,
MessageComponentTypes,
ButtonStyleTypes,
} from 'discord-interactions';
import { activeSearchs, skins } from '../../game/state.js';
import { getAllSkins } from '../../database/index.js';
} from "discord-interactions";
import { activeSearchs, skins } from "../../game/state.js";
import { getAllSkins } from "../../database/index.js";
/**
* Handles the /search slash command.
@@ -24,9 +24,9 @@ export async function handleSearchCommand(req, res, client, interactionId) {
try {
// --- 1. Fetch and Filter Data ---
const allDbSkins = getAllSkins.all();
const resultSkins = allDbSkins.filter((skin) =>
skin.displayName.toLowerCase().includes(searchValue) ||
skin.tierText.toLowerCase().includes(searchValue)
const resultSkins = allDbSkins.filter(
(skin) =>
skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue),
);
// --- 2. Handle No Results ---
@@ -34,7 +34,7 @@ export async function handleSearchCommand(req, res, client, interactionId) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Aucun skin ne correspond à votre recherche.',
content: "Aucun skin ne correspond à votre recherche.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
@@ -60,14 +60,14 @@ export async function handleSearchCommand(req, res, client, interactionId) {
}
// Fetch owner details if the skin is owned
let ownerText = '';
let ownerText = "";
if (currentSkin.user_id) {
try {
const owner = await guild.members.fetch(currentSkin.user_id);
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
} catch (e) {
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
ownerText = '| Appartenant à un utilisateur inconnu';
ownerText = "| Appartenant à un utilisateur inconnu";
}
}
@@ -88,20 +88,32 @@ export async function handleSearchCommand(req, res, client, interactionId) {
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_search_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
{ type: MessageComponentTypes.BUTTON, custom_id: `next_search_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
{
type: MessageComponentTypes.BUTTON,
custom_id: `prev_search_page_${interactionId}`,
label: "⏮️ Préc.",
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `next_search_page_${interactionId}`,
label: "Suiv. ⏭️",
style: ButtonStyleTypes.SECONDARY,
},
],
},
];
const embed = {
title: 'Résultats de la recherche',
title: "Résultats de la recherche",
description: `🔎 _"${searchValue}"_`,
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
fields: [{
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
fields: [
{
name: `**${currentSkin.displayName}**`,
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
}],
},
],
image: { url: getImageUrl(skinData) },
footer: { text: `Résultat 1/${resultSkins.length}` },
};
@@ -114,9 +126,8 @@ export async function handleSearchCommand(req, res, client, interactionId) {
components: components,
},
});
} catch (error) {
console.error('Error handling /search command:', error);
return res.status(500).json({ error: 'Failed to execute search.' });
console.error("Error handling /search command:", error);
return res.status(500).json({ error: "Failed to execute search." });
}
}

View File

@@ -1,5 +1,5 @@
import { InteractionResponseType } from 'discord-interactions';
import { getTopSkins } from '../../database/index.js';
import { InteractionResponseType } from "discord-interactions";
import { getTopSkins } from "../../database/index.js";
/**
* Handles the /skins slash command.
@@ -20,7 +20,7 @@ export async function handleSkinsCommand(req, res, client) {
// --- 2. Build Embed Fields Asynchronously ---
// We use a for...of loop to handle the async fetch for each owner.
for (const [index, skin] of topSkins.entries()) {
let ownerText = 'Libre'; // Default text if the skin has no owner
let ownerText = "Libre"; // Default text if the skin has no owner
// If the skin has an owner, fetch their details
if (skin.user_id) {
@@ -31,7 +31,7 @@ export async function handleSkinsCommand(req, res, client) {
} catch (e) {
// This can happen if the user has left the server
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`);
ownerText = 'Appartient à un utilisateur inconnu';
ownerText = "Appartient à un utilisateur inconnu";
}
}
@@ -49,20 +49,19 @@ export async function handleSkinsCommand(req, res, client) {
data: {
embeds: [
{
title: '🏆 Top 10 des Skins les Plus Chers',
description: 'Classement des skins par leur valeur maximale potentielle.',
title: "🏆 Top 10 des Skins les Plus Chers",
description: "Classement des skins par leur valeur maximale potentielle.",
fields: fields,
color: 0xFFD700, // Gold color for a leaderboard
color: 0xffd700, // Gold color for a leaderboard
footer: {
text: 'Utilisez /inventory pour voir vos propres skins.'
}
text: "Utilisez /inventory pour voir vos propres skins.",
},
},
],
},
});
} catch (error) {
console.error('Error handling /skins command:', error);
return res.status(500).json({ error: 'Failed to fetch the skins leaderboard.' });
console.error("Error handling /skins command:", error);
return res.status(500).json({ error: "Failed to fetch the skins leaderboard." });
}
}

View File

@@ -3,13 +3,13 @@ import {
InteractionResponseFlags,
MessageComponentTypes,
ButtonStyleTypes,
} from 'discord-interactions';
} from "discord-interactions";
import { formatTime, getOnlineUsersWithRole } from '../../utils/index.js';
import { DiscordRequest } from '../../api/discord.js';
import { activePolls } from '../../game/state.js';
import { getSocketIo } from '../../server/socket.js';
import { getUser } from '../../database/index.js';
import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from "../../game/state.js";
import { getSocketIo } from "../../server/socket.js";
import { getUser } from "../../database/index.js";
/**
* Handles the /timeout slash command.
@@ -34,7 +34,7 @@ export async function handleTimeoutCommand(req, res, client) {
// --- Validation Checks ---
// 1. Check if a poll is already running for the target user
const existingPoll = Object.values(activePolls).find(poll => poll.toUserId === targetUserId);
const existingPoll = Object.values(activePolls).find((poll) => poll.toUserId === targetUserId);
if (existingPoll) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
@@ -64,7 +64,7 @@ export async function handleTimeoutCommand(req, res, client) {
const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
const requiredMajority = Math.max(
parseInt(process.env.MIN_VOTES, 10),
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1,
);
// Store poll data in the active state
@@ -102,109 +102,141 @@ export async function handleTimeoutCommand(req, res, client) {
if (remaining === 0) {
clearInterval(countdownInterval);
const votersList = poll.voters.map(voterId => {
const votersList = poll.voters
.map((voterId) => {
const user = getUser.get(voterId);
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
}).join('\n');
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
})
.join("\n");
try {
await DiscordRequest(poll.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
embeds: [
{
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
description: `Il manquait **${votesNeeded}** vote(s).`,
fields: [{
name: 'Pour',
fields: [
{
name: "Pour",
value: `${poll.for}\n${votersList}`,
inline: true,
}],
color: 0xFF4444, // Red for failure
}],
},
],
color: 0xff4444, // Red for failure
},
],
components: [], // Remove buttons
},
});
} catch (err) {
console.error('Error updating failed poll message:', err);
console.error("Error updating failed poll message:", err);
}
// Clean up the poll from active state
delete activePolls[pollId];
io.emit('poll-update'); // Notify frontend
io.emit("poll-update"); // Notify frontend
return;
}
// --- Periodic Update Logic ---
// Update the message every second with the new countdown
try {
const votersList = poll.voters.map(voterId => {
const votersList = poll.voters
.map((voterId) => {
const user = getUser.get(voterId);
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
}).join('\n');
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
})
.join("\n");
await DiscordRequest(poll.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
title: 'Vote de Timeout',
embeds: [
{
title: "Vote de Timeout",
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
fields: [{
name: 'Pour',
fields: [
{
name: "Pour",
value: `${poll.for}\n${votersList}`,
inline: true,
}, {
name: 'Temps restant',
},
{
name: "Temps restant",
value: `${countdownText}`,
inline: false,
}],
color: 0x5865F2, // Discord Blurple
}],
},
],
color: 0x5865f2, // Discord Blurple
},
],
// Keep the components so people can still vote
components: [{
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_for_${pollId}`,
label: "Oui ✅",
style: ButtonStyleTypes.SUCCESS,
},
],
},
],
}],
},
});
} catch (err) {
console.error('Error updating countdown:', err);
console.error("Error updating countdown:", err);
// If the message was deleted, stop trying to update it.
if (err.message.includes('Unknown Message')) {
if (err.message.includes("Unknown Message")) {
clearInterval(countdownInterval);
delete activePolls[pollId];
io.emit('poll-update');
io.emit("poll-update");
}
}
}, 2000); // Update every 2 seconds to avoid rate limits
// --- Send Initial Response ---
io.emit('poll-update'); // Notify frontend
io.emit("poll-update"); // Notify frontend
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [{
title: 'Vote de Timeout',
embeds: [
{
title: "Vote de Timeout",
description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
fields: [{
name: 'Pour',
value: '✅ 0',
fields: [
{
name: "Pour",
value: "✅ 0",
inline: true,
}, {
name: 'Temps restant',
},
{
name: "Temps restant",
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
inline: false,
}],
color: 0x5865F2,
}],
components: [{
},
],
color: 0x5865f2,
},
],
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_for_${pollId}`,
label: "Oui ✅",
style: ButtonStyleTypes.SUCCESS,
},
],
},
],
}],
},
});
}

View File

@@ -1,13 +1,10 @@
import {
InteractionResponseType,
InteractionResponseFlags,
} from 'discord-interactions';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { postAPOBuy } from '../../utils/index.js';
import { DiscordRequest } from '../../api/discord.js';
import {getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
import { skins } from '../../game/state.js';
import { postAPOBuy } from "../../utils/index.js";
import { DiscordRequest } from "../../api/discord.js";
import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
import { skins } from "../../game/state.js";
/**
* Handles the /valorant slash command for opening a "skin case".
@@ -47,7 +44,7 @@ export async function handleValorantCommand(req, res, client) {
insertLog.run({
id: `${userId}-${Date.now()}`,
user_id: userId,
action: 'VALO_CASE_OPEN',
action: "VALO_CASE_OPEN",
target_user_id: null,
coins_amount: -valoPrice,
user_new_amount: commandUser.coins - valoPrice,
@@ -55,21 +52,20 @@ export async function handleValorantCommand(req, res, client) {
updateUserCoins.run({
id: userId,
coins: commandUser.coins - valoPrice,
})
});
// --- 2. Send Initial "Opening" Response ---
// Acknowledge the interaction immediately with a loading message.
const initialEmbed = new EmbedBuilder()
.setTitle('Ouverture de la caisse...')
.setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif')
.setColor('#F2F3F3');
.setTitle("Ouverture de la caisse...")
.setImage("https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif")
.setColor("#F2F3F3");
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { embeds: [initialEmbed] },
});
// --- 3. Run the skin reveal logic after a delay ---
setTimeout(async () => {
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
@@ -96,8 +92,8 @@ export async function handleValorantCommand(req, res, client) {
// --- Calculate Price ---
const calculatePrice = () => {
let result = parseFloat(dbSkin.basePrice);
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
result *= (1 + (randomChroma / 4));
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= 1 + randomChroma / 4;
return parseFloat(result.toFixed(0));
};
const finalPrice = calculatePrice();
@@ -117,30 +113,29 @@ export async function handleValorantCommand(req, res, client) {
// --- Edit the Original Message with the Result ---
await DiscordRequest(webhookEndpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [finalEmbed],
components: components,
},
});
} catch (revealError) {
console.error('Error during skin reveal:', revealError);
console.error("Error during skin reveal:", revealError);
// Inform the user that something went wrong
await DiscordRequest(webhookEndpoint, {
method: 'PATCH',
method: "PATCH",
body: {
content: "Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.",
content:
"Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.",
embeds: [],
},
});
}
}, 5000); // 5-second delay for suspense
} catch (error) {
console.error('Error handling /valorant command:', error);
console.error("Error handling /valorant command:", error);
// This catches errors from the initial interaction, e.g., the payment API call.
return res.status(500).json({ error: 'Failed to initiate the case opening.' });
return res.status(500).json({ error: "Failed to initiate the case opening." });
}
}
@@ -152,11 +147,14 @@ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
const getChromaName = () => {
if (chroma > 1) {
const name = selectedChromaData.displayName?.replace(/[\r\n]+/g, ' ').replace(skinData.displayName, '').trim();
const name = selectedChromaData.displayName
?.replace(/[\r\n]+/g, " ")
.replace(skinData.displayName, "")
.trim();
const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
return match ? match[1].trim() : (name || 'Chroma Inconnu');
return match ? match[1].trim() : name || "Chroma Inconnu";
}
return 'Base';
return "Base";
};
const getImageUrl = () => {
@@ -167,14 +165,15 @@ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
return levelData?.displayIcon || skinData.displayIcon;
};
const lvlText = (level >= 1 ? '1⃣' : '') +
(level >= 2 ? '2️⃣' : '') +
(level >= 3 ? '3️⃣' : '') +
(level >= 4 ? '4️⃣' : '') +
(level >= 5 ? '5️⃣' : '') +
(level >= 6 ? '6️⃣' : '') +
'◾'.repeat(skinData.levels.length - level);
const chromaText = '💠'.repeat(chroma) + '◾'.repeat(skinData.chromas.length - chroma);
const lvlText =
(level >= 1 ? "1️⃣" : "") +
(level >= 2 ? "2️⃣" : "") +
(level >= 3 ? "3️⃣" : "") +
(level >= 4 ? "4️⃣" : "") +
(level >= 5 ? "5️⃣" : "") +
(level >= 6 ? "6⃣" : "") +
"◾".repeat(skinData.levels.length - level);
const chromaText = "💠".repeat(chroma) + "◾".repeat(skinData.chromas.length - chroma);
return new EmbedBuilder()
.setTitle(`${skinData.displayName} | ${getChromaName()}`)
@@ -182,11 +181,15 @@ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
.setColor(`#${dbSkin.tierColor}`)
.setImage(getImageUrl())
.setFields([
{ name: 'Lvl', value: lvlText || 'N/A', inline: true },
{ name: 'Chroma', value: chromaText || 'N/A', inline: true },
{ name: 'Prix', value: `**${price}** <:vp:1362964205808128122>`, inline: true },
{ name: "Lvl", value: lvlText || "N/A", inline: true },
{ name: "Chroma", value: chromaText || "N/A", inline: true },
{
name: "Prix",
value: `**${price}** <:vp:1362964205808128122>`,
inline: true,
},
])
.setFooter({ text: 'Skin ajouté à votre inventaire !' });
.setFooter({ text: "Skin ajouté à votre inventaire !" });
}
/** Builds the action row with a video button if a video is available. */
@@ -203,11 +206,8 @@ function buildComponents(skinData, level, chroma) {
if (videoUrl) {
return [
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('🎬 Aperçu Vidéo')
.setStyle(ButtonStyle.Link)
.setURL(videoUrl)
)
new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl),
),
];
}
return []; // Return an empty array if no video is available

View File

@@ -3,10 +3,10 @@ import {
MessageComponentTypes,
ButtonStyleTypes,
InteractionResponseFlags,
} from 'discord-interactions';
} from "discord-interactions";
import { DiscordRequest } from '../../api/discord.js';
import { activeInventories, skins } from '../../game/state.js';
import { DiscordRequest } from "../../api/discord.js";
import { activeInventories, skins } from "../../game/state.js";
/**
* Handles navigation button clicks (Previous/Next) for the inventory embed.
@@ -19,7 +19,7 @@ export async function handleInventoryNav(req, res, client) {
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('_');
const [direction, page, interactionId] = custom_id.split("_");
// --- 1. Retrieve the interactive session ---
const inventorySession = activeInventories[interactionId];
@@ -46,16 +46,14 @@ export async function handleInventoryNav(req, res, client) {
});
}
// --- 3. Update Page Number ---
const { amount } = inventorySession;
if (direction === 'next') {
if (direction === "next") {
inventorySession.page = (inventorySession.page + 1) % amount;
} else if (direction === 'prev') {
} else if (direction === "prev") {
inventorySession.page = (inventorySession.page - 1 + amount) % amount;
}
try {
// --- 4. Rebuild Embed with New Page Content ---
const { page, inventorySkins } = inventorySession;
@@ -73,18 +71,21 @@ export async function handleInventoryNav(req, res, client) {
const getChromaText = (skin, skinInfo) => {
let result = "";
for (let i = 1; i <= skinInfo.chromas.length; i++) {
result += skin.currentChroma === i ? '💠 ' : '';
result += skin.currentChroma === i ? "💠 " : "";
}
return result || 'N/A';
return result || "N/A";
};
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 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';
return "Base";
};
const getImageUrl = (skin, skinInfo) => {
@@ -98,11 +99,22 @@ export async function handleInventoryNav(req, res, client) {
// --- 5. Rebuild Components (Buttons) ---
let components = [
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
{
type: MessageComponentTypes.BUTTON,
custom_id: `prev_page_${interactionId}`,
label: "⏮️ Préc.",
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `next_page_${interactionId}`,
label: "Suiv. ⏭️",
style: ButtonStyleTypes.SECONDARY,
},
];
const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
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({
@@ -115,26 +127,38 @@ export async function handleInventoryNav(req, res, client) {
// --- 6. Send PATCH Request to Update the Message ---
await DiscordRequest(inventorySession.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
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: [{
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) },
}],
components: [{ type: MessageComponentTypes.ACTION_ROW, components: components },
{ type: MessageComponentTypes.ACTION_ROW,
components: [{
},
],
components: [
{ type: MessageComponentTypes.ACTION_ROW, components: components },
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
label: 'Voir sur FlopoSite',
style: ButtonStyleTypes.LINK,}]
}],
label: "Voir sur FlopoSite",
style: ButtonStyleTypes.LINK,
},
],
},
],
},
});
@@ -142,17 +166,16 @@ export async function handleInventoryNav(req, res, client) {
// 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);
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: {
content: 'Une erreur est survenue lors de la mise à jour de l\'inventaire.',
content: "Une erreur est survenue lors de la mise à jour de l'inventaire.",
flags: InteractionResponseFlags.EPHEMERAL,
}
},
});
}
}

View File

@@ -1,11 +1,8 @@
import {
InteractionResponseType,
InteractionResponseFlags,
} from 'discord-interactions';
import { DiscordRequest } from '../../api/discord.js';
import { activePolls } from '../../game/state.js';
import { getSocketIo } from '../../server/socket.js';
import { getUser } from '../../database/index.js';
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from "../../game/state.js";
import { getSocketIo } from "../../server/socket.js";
import { getUser } from "../../database/index.js";
/**
* Handles clicks on the 'Yes' or 'No' buttons of a timeout poll.
@@ -18,8 +15,8 @@ export async function handlePollVote(req, res) {
const { custom_id } = data;
// --- 1. Parse Component ID ---
const [_, voteType, pollId] = custom_id.split('_'); // e.g., ['vote', 'for', '12345...']
const isVotingFor = voteType === 'for';
const [_, voteType, pollId] = custom_id.split("_"); // e.g., ['vote', 'for', '12345...']
const isVotingFor = voteType === "for";
// --- 2. Retrieve Poll and Validate ---
const poll = activePolls[pollId];
@@ -62,7 +59,7 @@ export async function handlePollVote(req, res) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Vous avez déjà voté pour ce sondage.',
content: "Vous avez déjà voté pour ce sondage.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
@@ -76,10 +73,9 @@ export async function handlePollVote(req, res) {
poll.against++;
}
io.emit('poll-update'); // Notify frontend clients of the change
const votersList = poll.voters.map(vId => `- ${getUser.get(vId)?.globalName || 'Utilisateur Inconnu'}`).join('\n');
io.emit("poll-update"); // Notify frontend clients of the change
const votersList = poll.voters.map((vId) => `- ${getUser.get(vId)?.globalName || "Utilisateur Inconnu"}`).join("\n");
// --- 4. Check for Majority ---
if (isVotingFor && poll.for >= poll.requiredMajority) {
@@ -88,19 +84,27 @@ export async function handlePollVote(req, res) {
// a. Update the poll message to show success
try {
await DiscordRequest(poll.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
title: 'Vote Terminé - Timeout Appliqué !',
embeds: [
{
title: "Vote Terminé - Timeout Appliqué !",
description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`,
fields: [{ name: 'Votes Pour', value: `${poll.for}\n${votersList}`, inline: true }],
color: 0x22A55B, // Green for success
}],
fields: [
{
name: "Votes Pour",
value: `${poll.for}\n${votersList}`,
inline: true,
},
],
color: 0x22a55b, // Green for success
},
],
components: [], // Remove buttons
},
});
} catch (err) {
console.error('Error updating final poll message:', err);
console.error("Error updating final poll message:", err);
}
// b. Execute the timeout via Discord API
@@ -108,24 +112,23 @@ export async function handlePollVote(req, res) {
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
await DiscordRequest(endpointTimeout, {
method: 'PATCH',
method: "PATCH",
body: { communication_disabled_until: timeoutUntil },
});
// c. Send a public confirmation message and clean up
delete activePolls[pollId];
io.emit('poll-update');
io.emit("poll-update");
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`,
},
});
} catch (err) {
console.error('Error timing out user:', err);
console.error("Error timing out user:", err);
delete activePolls[pollId];
io.emit('poll-update');
io.emit("poll-update");
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
@@ -140,7 +143,7 @@ export async function handlePollVote(req, res) {
res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Votre vote a été enregistré ! ✅',
content: "Votre vote a été enregistré ! ✅",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
@@ -152,25 +155,30 @@ export async function handlePollVote(req, res) {
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
DiscordRequest(poll.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
title: 'Vote de Timeout',
embeds: [
{
title: "Vote de Timeout",
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
fields: [{
name: 'Pour',
fields: [
{
name: "Pour",
value: `${poll.for}\n${votersList}`,
inline: true,
}, {
name: 'Temps restant',
},
{
name: "Temps restant",
value: `${countdownText}`,
inline: false,
}],
color: 0x5865F2,
}],
},
],
color: 0x5865f2,
},
],
// Keep the original components so people can still vote
components: req.body.message.components,
},
}).catch(err => console.error("Error updating poll after vote:", err));
}).catch((err) => console.error("Error updating poll after vote:", err));
}
}

View File

@@ -3,10 +3,10 @@ import {
InteractionResponseFlags,
MessageComponentTypes,
ButtonStyleTypes,
} from 'discord-interactions';
} from "discord-interactions";
import { DiscordRequest } from '../../api/discord.js';
import { activeSearchs, skins } from '../../game/state.js';
import { DiscordRequest } from "../../api/discord.js";
import { activeSearchs, skins } from "../../game/state.js";
/**
* Handles navigation button clicks (Previous/Next) for the search results embed.
@@ -19,7 +19,7 @@ export async function handleSearchNav(req, res, client) {
const { custom_id } = data;
// Extract direction and the original interaction ID from the custom_id
const [direction, _, page, interactionId] = custom_id.split('_'); // e.g., ['next', 'search', 'page', '123...']
const [direction, _, page, interactionId] = custom_id.split("_"); // e.g., ['next', 'search', 'page', '123...']
// --- 1. Retrieve the interactive session ---
const searchSession = activeSearchs[interactionId];
@@ -48,9 +48,9 @@ export async function handleSearchNav(req, res, client) {
// --- 3. Update Page Number ---
const { amount } = searchSession;
if (direction === 'next') {
if (direction === "next") {
searchSession.page = (searchSession.page + 1) % amount;
} else if (direction === 'prev') {
} else if (direction === "prev") {
searchSession.page = (searchSession.page - 1 + amount) % amount;
}
@@ -64,14 +64,14 @@ export async function handleSearchNav(req, res, client) {
}
// Fetch owner details if the skin is owned
let ownerText = '';
let ownerText = "";
if (currentSkin.user_id) {
try {
const owner = await client.users.fetch(currentSkin.user_id);
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
} catch (e) {
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
ownerText = '| Appartenant à un utilisateur inconnu';
ownerText = "| Appartenant à un utilisateur inconnu";
}
}
@@ -88,34 +88,37 @@ export async function handleSearchNav(req, res, client) {
// --- 5. Send PATCH Request to Update the Message ---
// Note: The components (buttons) do not change, so we can reuse them from the original message.
await DiscordRequest(searchSession.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
title: 'Résultats de la recherche',
embeds: [
{
title: "Résultats de la recherche",
description: `🔎 _"${searchValue}"_`,
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
fields: [{
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
fields: [
{
name: `**${currentSkin.displayName}**`,
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
}],
},
],
image: { url: getImageUrl(skinData) },
footer: { text: `Résultat ${page + 1}/${amount}` },
}],
},
],
components: req.body.message.components, // Reuse existing components
},
});
// --- 6. Acknowledge the Interaction ---
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
} catch (error) {
console.error('Error handling search navigation:', error);
console.error("Error handling search navigation:", error);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Une erreur est survenue lors de la mise à jour de la recherche.',
content: "Une erreur est survenue lors de la mise à jour de la recherche.",
flags: InteractionResponseFlags.EPHEMERAL,
}
},
});
}
}

View File

@@ -3,13 +3,13 @@ import {
InteractionResponseFlags,
MessageComponentTypes,
ButtonStyleTypes,
} from 'discord-interactions';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
} from "discord-interactions";
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { DiscordRequest } from '../../api/discord.js';
import { postAPOBuy } from '../../utils/index.js';
import { activeInventories, skins } from '../../game/state.js';
import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
import { DiscordRequest } from "../../api/discord.js";
import { postAPOBuy } from "../../utils/index.js";
import { activeInventories, skins } from "../../game/state.js";
import { getSkin, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
/**
* Handles the click of the 'Upgrade' button on a skin in the inventory.
@@ -20,7 +20,7 @@ export async function handleUpgradeSkin(req, res) {
const { member, data } = req.body;
const { custom_id } = data;
const interactionId = custom_id.replace('upgrade_', '');
const interactionId = custom_id.replace("upgrade_", "");
const userId = member.user.id;
// --- 1. Retrieve Session and Validate ---
@@ -28,7 +28,10 @@ export async function handleUpgradeSkin(req, res) {
if (!inventorySession) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { content: "Cet affichage d'inventaire a expiré.", flags: InteractionResponseFlags.EPHEMERAL },
data: {
content: "Cet affichage d'inventaire a expiré.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
@@ -36,21 +39,29 @@ export async function handleUpgradeSkin(req, res) {
if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.", flags: InteractionResponseFlags.EPHEMERAL },
data: {
content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page];
const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid);
if (!skinData || (skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)) {
if (
!skinData ||
(skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)
) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.", flags: InteractionResponseFlags.EPHEMERAL },
data: {
content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
// --- 2. Handle Payment ---
const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10;
@@ -78,7 +89,7 @@ export async function handleUpgradeSkin(req, res) {
insertLog.run({
id: `${userId}-${Date.now()}`,
user_id: userId,
action: 'VALO_SKIN_UPGRADE',
action: "VALO_SKIN_UPGRADE",
target_user_id: null,
coins_amount: -upgradePrice.toFixed(0),
user_new_amount: commandUser.coins - upgradePrice.toFixed(0),
@@ -86,40 +97,44 @@ export async function handleUpgradeSkin(req, res) {
updateUserCoins.run({
id: userId,
coins: commandUser.coins - upgradePrice.toFixed(0),
})
});
// --- 3. Show Loading Animation ---
// Acknowledge the click immediately and then edit the message to show a loading state.
await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
await DiscordRequest(inventorySession.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [{
title: 'Amélioration en cours...',
image: { url: 'https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif' },
color: 0x4F545C,
}],
embeds: [
{
title: "Amélioration en cours...",
image: {
url: "https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif",
},
color: 0x4f545c,
},
],
components: [],
},
});
// --- 4. Perform Upgrade Logic ---
let succeeded = false;
const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length;
if (isLevelUpgrade) {
// Upgrading Level
const successProb = 1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
const successProb =
1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
if (Math.random() < successProb) {
succeeded = true;
skinToUpgrade.currentLvl++;
}
} else {
// Upgrading Chroma
const successProb = 1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
const successProb =
1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
if (Math.random() < successProb) {
succeeded = true;
skinToUpgrade.currentChroma++;
@@ -130,8 +145,8 @@ export async function handleUpgradeSkin(req, res) {
if (succeeded) {
const calculatePrice = () => {
let result = parseFloat(skinToUpgrade.basePrice);
result *= (1 + (skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2)));
result *= (1 + (skinToUpgrade.currentChroma / 4));
result *= 1 + skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2);
result *= 1 + skinToUpgrade.currentChroma / 4;
return parseFloat(result.toFixed(0));
};
skinToUpgrade.currentPrice = calculatePrice();
@@ -147,7 +162,6 @@ export async function handleUpgradeSkin(req, res) {
inventorySession.inventorySkins[inventorySession.page] = skinToUpgrade;
}
// --- 6. Send Final Result ---
setTimeout(async () => {
// Fetch the latest state of the skin from the database
@@ -156,7 +170,7 @@ export async function handleUpgradeSkin(req, res) {
const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);
await DiscordRequest(inventorySession.endpoint, {
method: 'PATCH',
method: "PATCH",
body: {
embeds: [finalEmbed],
components: finalComponents,
@@ -170,19 +184,31 @@ export async function handleUpgradeSkin(req, res) {
/** Builds the result embed (success or failure). */
function buildFinalEmbed(succeeded, skin, skinData) {
const embed = new EmbedBuilder()
.setTitle(succeeded ? 'Amélioration Réussie ! 🎉' : "L'amélioration a échoué... ❌")
.setTitle(succeeded ? "Amélioration Réussie ! 🎉" : "L'amélioration a échoué... ❌")
.setDescription(`**${skin.displayName}**`)
.setImage(skin.displayIcon) // A static image is fine here
.setColor(succeeded ? 0x22A55B : 0xED4245);
.setColor(succeeded ? 0x22a55b : 0xed4245);
if (succeeded) {
embed.addFields(
{ name: 'Nouveau Niveau', value: `${skin.currentLvl}/${skinData.levels.length}`, inline: true },
{ name: 'Nouveau Chroma', value: `${skin.currentChroma}/${skinData.chromas.length}`, inline: true },
{ name: 'Nouvelle Valeur', value: `**${skin.currentPrice} Flopos**`, inline: true }
{
name: "Nouveau Niveau",
value: `${skin.currentLvl}/${skinData.levels.length}`,
inline: true,
},
{
name: "Nouveau Chroma",
value: `${skin.currentChroma}/${skinData.chromas.length}`,
inline: true,
},
{
name: "Nouvelle Valeur",
value: `**${skin.currentPrice} Flopos**`,
inline: true,
},
);
} else {
embed.addFields({ name: 'Statut', value: 'Aucun changement.' });
embed.addFields({ name: "Statut", value: "Aucun changement." });
}
return embed;
}
@@ -201,7 +227,7 @@ function buildFinalComponents(succeeded, skinData, skin, interactionId) {
const videoUrl = levelData.streamedVideo || chromaData.streamedVideo;
if (videoUrl) {
row.addComponents(new ButtonBuilder().setLabel('🎬 Aperçu Vidéo').setStyle(ButtonStyle.Link).setURL(videoUrl));
row.addComponents(new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl));
} else {
return []; // No button if no video
}
@@ -209,9 +235,9 @@ function buildFinalComponents(succeeded, skinData, skin, interactionId) {
// Add a "Retry" button
row.addComponents(
new ButtonBuilder()
.setLabel('Réessayer 🔄️')
.setLabel("Réessayer 🔄️")
.setStyle(ButtonStyle.Primary)
.setCustomId(`upgrade_${interactionId}`)
.setCustomId(`upgrade_${interactionId}`),
);
}
return [row];

View File

@@ -1,5 +1,5 @@
import { handleMessageCreate } from './handlers/messageCreate.js';
import { getAkhys, setupCronJobs } from '../utils/index.js';
import { handleMessageCreate } from "./handlers/messageCreate.js";
import { getAkhys, setupCronJobs } from "../utils/index.js";
/**
* Initializes and attaches all necessary event listeners to the Discord client.
@@ -12,19 +12,19 @@ export function initializeEvents(client, io) {
// --- on 'ready' ---
// This event fires once the bot has successfully logged in and is ready to operate.
// It's a good place for setup tasks that require the bot to be online.
client.once('ready', async () => {
client.once("clientReady", async () => {
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
console.log('[Startup] Bot is ready, performing initial data sync...');
console.log("[Startup] Bot is ready, performing initial data sync...");
await getAkhys(client);
console.log('[Startup] Setting up scheduled tasks...');
console.log("[Startup] Setting up scheduled tasks...");
setupCronJobs(client, io);
console.log('--- FlopoBOT is fully operational ---');
console.log("--- FlopoBOT is fully operational ---");
});
// --- on 'messageCreate' ---
// This event fires every time a message is sent in a channel the bot can see.
// The logic is delegated to its own dedicated handler for cleanliness.
client.on('messageCreate', async (message) => {
client.on("messageCreate", async (message) => {
// We pass the client and io instances to the handler so it has access to them
// without needing to import them, preventing potential circular dependencies.
await handleMessageCreate(message, client, io);

View File

@@ -1,22 +1,19 @@
import {
InteractionType,
InteractionResponseType,
} from 'discord-interactions';
import { InteractionType, InteractionResponseType } from "discord-interactions";
// --- Command Handlers ---
import { handleTimeoutCommand } from '../commands/timeout.js';
import { handleInventoryCommand } from '../commands/inventory.js';
import { handleValorantCommand } from '../commands/valorant.js';
import { handleInfoCommand } from '../commands/info.js';
import { handleSkinsCommand } from '../commands/skins.js';
import { handleSearchCommand } from '../commands/search.js';
import { handleFlopoSiteCommand } from '../commands/floposite.js';
import { handleTimeoutCommand } from "../commands/timeout.js";
import { handleInventoryCommand } from "../commands/inventory.js";
import { handleValorantCommand } from "../commands/valorant.js";
import { handleInfoCommand } from "../commands/info.js";
import { handleSkinsCommand } from "../commands/skins.js";
import { handleSearchCommand } from "../commands/search.js";
import { handleFlopoSiteCommand } from "../commands/floposite.js";
// --- Component Handlers ---
import { handlePollVote } from '../components/pollVote.js';
import { handleInventoryNav } from '../components/inventoryNav.js';
import { handleUpgradeSkin } from '../components/upgradeSkin.js';
import { handleSearchNav } from '../components/searchNav.js';
import { handlePollVote } from "../components/pollVote.js";
import { handleInventoryNav } from "../components/inventoryNav.js";
import { handleUpgradeSkin } from "../components/upgradeSkin.js";
import { handleSearchNav } from "../components/searchNav.js";
/**
* The main handler for all incoming interactions from Discord.
@@ -36,54 +33,53 @@ export async function handleInteraction(req, res, client) {
const { name } = data;
switch (name) {
case 'timeout':
case "timeout":
return await handleTimeoutCommand(req, res, client);
case 'inventory':
case "inventory":
return await handleInventoryCommand(req, res, client, id);
case 'valorant':
case "valorant":
return await handleValorantCommand(req, res, client);
case 'info':
case "info":
return await handleInfoCommand(req, res, client);
case 'skins':
case "skins":
return await handleSkinsCommand(req, res, client);
case 'search':
case "search":
return await handleSearchCommand(req, res, client, id);
case 'floposite':
case "floposite":
return await handleFlopoSiteCommand(req, res);
default:
console.error(`Unknown command: ${name}`);
return res.status(400).json({ error: 'Unknown command' });
return res.status(400).json({ error: "Unknown command" });
}
}
if (type === InteractionType.MESSAGE_COMPONENT) {
const componentId = data.custom_id;
if (componentId.startsWith('vote_')) {
if (componentId.startsWith("vote_")) {
return await handlePollVote(req, res, client);
}
if (componentId.startsWith('prev_page') || componentId.startsWith('next_page')) {
if (componentId.startsWith("prev_page") || componentId.startsWith("next_page")) {
return await handleInventoryNav(req, res, client);
}
if (componentId.startsWith('upgrade_')) {
if (componentId.startsWith("upgrade_")) {
return await handleUpgradeSkin(req, res, client);
}
if (componentId.startsWith('prev_search_page') || componentId.startsWith('next_search_page')) {
if (componentId.startsWith("prev_search_page") || componentId.startsWith("next_search_page")) {
return await handleSearchNav(req, res, client);
}
// Fallback for other potential components
console.error(`Unknown component ID: ${componentId}`);
return res.status(400).json({ error: 'Unknown component' });
return res.status(400).json({ error: "Unknown component" });
}
// --- Fallback for Unknown Interaction Types ---
console.error('Unknown interaction type:', type);
return res.status(400).json({ error: 'Unknown interaction type' });
console.error("Unknown interaction type:", type);
return res.status(400).json({ error: "Unknown interaction type" });
} catch (error) {
console.error('Error handling interaction:', error);
console.error("Error handling interaction:", error);
// Send a generic error response to Discord if something goes wrong
return res.status(500).json({ error: 'An internal error occurred' });
return res.status(500).json({ error: "An internal error occurred" });
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import 'dotenv/config';
import { getTimesChoices } from '../game/various.js';
import { capitalize, InstallGlobalCommands } from '../utils/index.js';
import "dotenv/config";
import { getTimesChoices } from "../game/various.js";
import { capitalize, InstallGlobalCommands } from "../utils/index.js";
function createTimesChoices() {
const choices = getTimesChoices();
@@ -18,95 +18,103 @@ function createTimesChoices() {
// Timeout vote command
const TIMEOUT_COMMAND = {
name: 'timeout',
description: 'Vote démocratique pour timeout un boug',
name: "timeout",
description: "Vote démocratique pour timeout un boug",
options: [
{
type: 6,
name: 'akhy',
description: 'Qui ?',
name: "akhy",
description: "Qui ?",
required: true,
},
{
type: 3,
name: 'temps',
description: 'Combien de temps ?',
name: "temps",
description: "Combien de temps ?",
required: true,
choices: createTimesChoices(),
}
},
],
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
// Valorant
const VALORANT_COMMAND = {
name: 'valorant',
name: "valorant",
description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
// Own inventory command
const INVENTORY_COMMAND = {
name: 'inventory',
description: 'Voir inventaire',
name: "inventory",
description: "Voir inventaire",
options: [
{
type: 6,
name: 'akhy',
description: 'Qui ?',
name: "akhy",
description: "Qui ?",
required: false,
},
],
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
const INFO_COMMAND = {
name: 'info',
description: 'Qui est time out ?',
name: "info",
description: "Qui est time out ?",
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
const SKINS_COMMAND = {
name: 'skins',
description: 'Le top 10 des skins les plus chers.',
name: "skins",
description: "Le top 10 des skins les plus chers.",
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
const SITE_COMMAND = {
name: 'floposite',
description: 'Lien vers FlopoSite',
name: "floposite",
description: "Lien vers FlopoSite",
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
const SEARCH_SKIN_COMMAND = {
name: 'search',
description: 'Chercher un skin',
name: "search",
description: "Chercher un skin",
options: [
{
type: 3,
name: 'recherche',
description: 'Tu cherches quoi ?',
name: "recherche",
description: "Tu cherches quoi ?",
required: true,
},
],
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
}
};
const ALL_COMMANDS = [TIMEOUT_COMMAND, INVENTORY_COMMAND, VALORANT_COMMAND, INFO_COMMAND, SKINS_COMMAND, SEARCH_SKIN_COMMAND, SITE_COMMAND];
const ALL_COMMANDS = [
TIMEOUT_COMMAND,
INVENTORY_COMMAND,
VALORANT_COMMAND,
INFO_COMMAND,
SKINS_COMMAND,
SEARCH_SKIN_COMMAND,
SITE_COMMAND,
];
export function registerCommands() {
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);

View File

@@ -1,10 +1,10 @@
import Database from "better-sqlite3";
export const flopoDB = new Database('flopobot.db');
export const flopoDB = new Database("flopobot.db");
export const stmtUsers = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS users
(
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
globalName TEXT,
@@ -20,7 +20,8 @@ export const stmtUsers = flopoDB.prepare(`
`);
stmtUsers.run();
export const stmtSkins = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS skins (
CREATE TABLE IF NOT EXISTS skins
(
uuid TEXT PRIMARY KEY,
displayName TEXT,
contentTierUuid TEXT,
@@ -36,44 +37,198 @@ export const stmtSkins = flopoDB.prepare(`
maxPrice INTEGER DEFAULT NULL
)
`);
stmtSkins.run()
stmtSkins.run();
export const insertUser = flopoDB.prepare('INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy) VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)');
export const updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id');
export const updateUserAvatar = flopoDB.prepare('UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id');
export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`);
export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0`);
export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id');
export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?');
export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC');
export const getAllAkhys = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC');
export const insertUser = flopoDB.prepare(
`INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
);
export const updateUser = flopoDB.prepare(
`UPDATE users
SET warned = @warned,
warns = @warns,
allTimeWarns = @allTimeWarns,
totalRequests = @totalRequests
WHERE id = @id`,
);
export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id");
export const queryDailyReward = flopoDB.prepare(`UPDATE users
SET dailyQueried = 1
WHERE id = ?`);
export const resetDailyReward = flopoDB.prepare(`UPDATE users
SET dailyQueried = 0`);
export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id");
export const getUser = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?",
);
export const getAllUsers = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC",
);
export const getAllAkhys = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC",
);
export const insertSkin = flopoDB.prepare('INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, basePrice, currentLvl, currentChroma, currentPrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)');
export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid');
export const hardUpdateSkin = flopoDB.prepare('UPDATE skins SET displayName = @displayName, contentTierUuid = @contentTierUuid, displayIcon = @displayIcon, tierRank = @tierRank, tierColor = @tierColor, tierText = @tierText, basePrice = @basePrice, user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice, maxPrice = @maxPrice WHERE uuid = @uuid');
export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?');
export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC');
export const getAllAvailableSkins = flopoDB.prepare('SELECT * FROM skins WHERE user_id IS NULL');
export const getUserInventory = flopoDB.prepare('SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC');
export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10');
export const insertSkin = flopoDB.prepare(
`INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
basePrice, currentLvl, currentChroma, currentPrice, maxPrice)
VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
@basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`,
);
export const updateSkin = flopoDB.prepare(
`UPDATE skins
SET user_id = @user_id,
currentLvl = @currentLvl,
currentChroma = @currentChroma,
currentPrice = @currentPrice
WHERE uuid = @uuid`,
);
export const hardUpdateSkin = flopoDB.prepare(
`UPDATE skins
SET displayName = @displayName,
contentTierUuid = @contentTierUuid,
displayIcon = @displayIcon,
tierRank = @tierRank,
tierColor = @tierColor,
tierText = @tierText,
basePrice = @basePrice,
user_id = @user_id,
currentLvl = @currentLvl,
currentChroma = @currentChroma,
currentPrice = @currentPrice,
maxPrice = @maxPrice
WHERE uuid = @uuid`,
);
export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?");
export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC");
export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL");
export const getUserInventory = flopoDB.prepare(
"SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC",
);
export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10");
export const stmtMarketOffers = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS market_offers
(
id PRIMARY KEY,
skin_uuid TEXT REFERENCES skins,
seller_id TEXT REFERENCES users,
starting_price INTEGER NOT NULL,
buyout_price INTEGER DEFAULT NULL,
final_price INTEGER DEFAULT NULL,
status TEXT NOT NULL,
posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
opening_at TIMESTAMP NOT NULL,
closing_at TIMESTAMP NOT NULL,
buyer_id TEXT REFERENCES users DEFAULT NULL
)
`);
stmtMarketOffers.run();
export const stmtBids = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS bids
(
id PRIMARY KEY,
bidder_id TEXT REFERENCES users,
market_offer_id REFERENCES market_offers,
offer_amount INTEGER,
offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
stmtBids.run();
export const getMarketOffers = flopoDB.prepare(`
SELECT market_offers.*,
skins.displayName AS skinName,
skins.displayIcon AS skinIcon,
seller.username AS sellerName,
seller.globalName AS sellerGlobalName,
buyer.username AS buyerName,
buyer.globalName AS buyerGlobalName
FROM market_offers
JOIN skins ON skins.uuid = market_offers.skin_uuid
JOIN users AS seller ON seller.id = market_offers.seller_id
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
ORDER BY market_offers.posted_at DESC
`);
export const getMarketOfferById = flopoDB.prepare(`
SELECT market_offers.*,
skins.displayName AS skinName,
skins.displayIcon AS skinIcon,
seller.username AS sellerName,
seller.globalName AS sellerGlobalName,
buyer.username AS buyerName,
buyer.globalName AS buyerGlobalName
FROM market_offers
JOIN skins ON skins.uuid = market_offers.skin_uuid
JOIN users AS seller ON seller.id = market_offers.seller_id
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
WHERE market_offers.id = ?
`);
export const insertMarketOffer = flopoDB.prepare(`
INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at)
VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at)
`);
export const getBids = flopoDB.prepare(`
SELECT bids.*,
bidder.username AS bidderName,
bidder.globalName AS bidderGlobalName
FROM bids
JOIN users AS bidder ON bidder.id = bids.bidder_id
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
`);
export const getBidById = flopoDB.prepare(`
SELECT bids.*
FROM bids
WHERE bids.id = ?
`);
export const getOfferBids = flopoDB.prepare(`
SELECT bids.*
FROM bids
WHERE bids.market_offer_id = ?
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
`);
export const insertBid = flopoDB.prepare(`
INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount)
VALUES (@id, @bidder_id, @market_offer_id, @offer_amount)
`);
export const insertManyUsers = flopoDB.transaction(async (users) => {
for (const user of users) try { await insertUser.run(user) } catch (e) { /**/ }
for (const user of users)
try {
await insertUser.run(user);
} catch (e) {}
});
export const updateManyUsers = flopoDB.transaction(async (users) => {
for (const user of users) try { await updateUser.run(user) } catch (e) { console.log('user update failed') }
for (const user of users)
try {
await updateUser.run(user);
} catch (e) {
console.log("user update failed");
}
});
export const insertManySkins = flopoDB.transaction(async (skins) => {
for (const skin of skins) try { await insertSkin.run(skin) } catch (e) {}
for (const skin of skins)
try {
await insertSkin.run(skin);
} catch (e) {}
});
export const updateManySkins = flopoDB.transaction(async (skins) => {
for (const skin of skins) try { await updateSkin.run(skin) } catch (e) {}
for (const skin of skins)
try {
await updateSkin.run(skin);
} catch (e) {}
});
export const stmtLogs = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS logs (
CREATE TABLE IF NOT EXISTS logs
(
id PRIMARY KEY,
user_id TEXT REFERENCES users,
action TEXT,
@@ -82,15 +237,18 @@ export const stmtLogs = flopoDB.prepare(`
user_new_amount INTEGER
)
`);
stmtLogs.run()
export const insertLog = flopoDB.prepare('INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)');
export const getLogs = flopoDB.prepare('SELECT * FROM logs');
export const getUserLogs = flopoDB.prepare('SELECT * FROM logs WHERE user_id = @user_id');
stmtLogs.run();
export const insertLog = flopoDB.prepare(
`INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount)
VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`,
);
export const getLogs = flopoDB.prepare("SELECT * FROM logs");
export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id");
export const stmtGames = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS games (
CREATE TABLE IF NOT EXISTS games
(
id PRIMARY KEY,
p1 TEXT REFERENCES users,
p2 TEXT REFERENCES users,
@@ -104,31 +262,42 @@ export const stmtGames = flopoDB.prepare(`
timestamp TIMESTAMP
)
`);
stmtGames.run()
export const insertGame = flopoDB.prepare('INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)');
export const getGames = flopoDB.prepare('SELECT * FROM games');
export const getUserGames = flopoDB.prepare('SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp');
stmtGames.run();
export const insertGame = flopoDB.prepare(
`INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp)
VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`,
);
export const getGames = flopoDB.prepare("SELECT * FROM games");
export const getUserGames = flopoDB.prepare(
"SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp",
);
export const stmtElos = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS elos (
CREATE TABLE IF NOT EXISTS elos
(
id PRIMARY KEY REFERENCES users,
elo INTEGER
)
`);
stmtElos.run()
stmtElos.run();
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) VALUES (@id, @elo)`);
export const getElos = flopoDB.prepare(`SELECT * FROM elos`);
export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`);
export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id');
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
VALUES (@id, @elo)`);
export const getElos = flopoDB.prepare(`SELECT *
FROM elos`);
export const getUserElo = flopoDB.prepare(`SELECT *
FROM elos
WHERE id = @id`);
export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id");
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC')
export const getUsersByElo = flopoDB.prepare(
"SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
);
export const stmtSOTD = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd (
CREATE TABLE IF NOT EXISTS sotd
(
id INT PRIMARY KEY,
tableauPiles TEXT,
foundationPiles TEXT,
@@ -138,14 +307,21 @@ export const stmtSOTD = flopoDB.prepare(`
seed TEXT
)
`);
stmtSOTD.run()
stmtSOTD.run();
export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`)
export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`)
export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`)
export const getSOTD = flopoDB.prepare(`SELECT *
FROM sotd
WHERE id = '0'`);
export const insertSOTD =
flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed)
VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`);
export const deleteSOTD = flopoDB.prepare(`DELETE
FROM sotd
WHERE id = '0'`);
export const stmtSOTDStats = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd_stats (
CREATE TABLE IF NOT EXISTS sotd_stats
(
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users,
time INTEGER,
@@ -153,38 +329,53 @@ export const stmtSOTDStats = flopoDB.prepare(`
score INTEGER
)
`);
stmtSOTDStats.run()
stmtSOTDStats.run();
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName FROM sotd_stats JOIN users ON users.id = sotd_stats.user_id ORDER BY score DESC, moves ASC, time ASC`);
export const getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`);
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`);
export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`);
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`);
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
FROM sotd_stats
JOIN users ON users.id = sotd_stats.user_id
ORDER BY score DESC, moves ASC, time ASC`);
export const getUserSOTDStats = flopoDB.prepare(`SELECT *
FROM sotd_stats
WHERE user_id = ?`);
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score)
VALUES (@id, @user_id, @time, @moves, @score)`);
export const clearSOTDStats = flopoDB.prepare(`DELETE
FROM sotd_stats`);
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE
FROM sotd_stats
WHERE user_id = ?`);
export async function pruneOldLogs() {
const users = flopoDB.prepare(`
const users = flopoDB
.prepare(
`
SELECT user_id
FROM logs
GROUP BY user_id
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
`).all();
`,
)
.all();
const transaction = flopoDB.transaction(() => {
for (const { user_id } of users) {
flopoDB.prepare(`
DELETE FROM logs
WHERE id IN (
SELECT id FROM (
SELECT id,
flopoDB
.prepare(
`
DELETE
FROM logs
WHERE id IN (SELECT id
FROM (SELECT id,
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
FROM logs
WHERE user_id = ?
WHERE user_id = ?)
WHERE rn > ${process.env.LOGS_BY_USER})
`,
)
WHERE rn > ${process.env.LOGS_BY_USER}
)
`).run(user_id);
.run(user_id);
}
});
transaction()
transaction();
}

View File

@@ -11,7 +11,7 @@ export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"];
export const SUITS = ["d", "s", "c", "h"];
// Build a single 52-card deck like "Ad","Ts", etc.
export const singleDeck = RANKS.flatMap(r => SUITS.map(s => `${r}${s}`));
export const singleDeck = RANKS.flatMap((r) => SUITS.map((s) => `${r}${s}`));
export function buildShoe(decks = 6) {
const shoe = [];
@@ -41,15 +41,17 @@ export function handValue(cards) {
let aces = 0;
for (const c of cards) {
const r = c[0];
if (r === "A") { total += 11; aces += 1; }
else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
if (r === "A") {
total += 11;
aces += 1;
} else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
else total += Number(r);
}
while (total > 21 && aces > 0) {
total -= 10; // convert an Ace from 11 to 1
aces -= 1;
}
const soft = (aces > 0); // if any Ace still counted as 11, it's a soft hand
const soft = aces > 0; // if any Ace still counted as 11, it's a soft hand
return { total, soft };
}
@@ -82,7 +84,14 @@ export function compareHands(playerCards, dealerCards) {
// Compute payout for a single finished hand (no splits here).
// options: { blackjackPayout: 1.5, allowSurrender: false }
export function settleHand({ bet, playerCards, dealerCards, doubled = false, surrendered = false, blackjackPayout = 1.5 }) {
export function settleHand({
bet,
playerCards,
dealerCards,
doubled = false,
surrendered = false,
blackjackPayout = 1.5,
}) {
if (surrendered) return { delta: -bet / 2, result: "surrender" };
const pBJ = isBlackjack(playerCards);
@@ -114,7 +123,7 @@ export function publicPlayerView(player) {
bank: player.bank,
currentBet: player.currentBet,
inRound: player.inRound,
hands: player.hands.map(h => ({
hands: player.hands.map((h) => ({
cards: h.cards,
stood: h.stood,
busted: h.busted,
@@ -146,7 +155,7 @@ export function createBlackjackRoom({
},
animation = {
dealerDrawMs: 500,
}
},
} = {}) {
return {
id: "blackjack-room",
@@ -154,8 +163,17 @@ export function createBlackjackRoom({
created_at: Date.now(),
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
phase_ends_at: Date.now() + phaseDurations.bettingMs,
minBet, maxBet, fakeMoney,
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation },
minBet,
maxBet,
fakeMoney,
settings: {
decks,
hitSoft17,
blackjackPayout,
cutCardRatio,
phaseDurations,
animation,
},
shoe: buildShoe(decks),
discard: [],
dealer: { cards: [], holeHidden: true },
@@ -179,7 +197,17 @@ export function resetForNewRound(room) {
for (const p of Object.values(room.players)) {
p.inRound = false;
p.currentBet = 0;
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 } ];
p.hands = [
{
cards: [],
stood: false,
busted: false,
doubled: false,
surrendered: false,
hasActed: false,
bet: 0,
},
];
p.activeHand = 0;
}
}
@@ -198,10 +226,20 @@ export function startBetting(room, now) {
export function dealInitial(room) {
room.status = "dealing";
// Deal one to each player who placed a bet, then again, then dealer up + hole
const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet);
const actives = Object.values(room.players).filter((p) => p.currentBet >= room.minBet);
for (const p of actives) {
p.inRound = true;
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: p.currentBet } ];
p.hands = [
{
cards: [draw(room.shoe)],
stood: false,
busted: false,
doubled: false,
surrendered: false,
hasActed: false,
bet: p.currentBet,
},
];
}
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
room.dealer.holeHidden = true;
@@ -224,9 +262,9 @@ export function autoActions(room) {
}
export function everyoneDone(room) {
return Object.values(room.players).every(p => {
return Object.values(room.players).every((p) => {
if (!p.inRound) return true;
return p.hands.filter(h => !h.stood && !h.busted && !h.surrendered)?.length === 0;
return p.hands.filter((h) => !h.stood && !h.busted && !h.surrendered)?.length === 0;
});
}
@@ -240,7 +278,7 @@ export function dealerPlay(room) {
export async function settleAll(room) {
room.status = "payout";
const allRes = {}
const allRes = {};
for (const p of Object.values(room.players)) {
if (!p.inRound) continue;
for (const hand of p.hands) {
@@ -258,23 +296,28 @@ export async function settleAll(room) {
allRes[p.id] = [res];
}
p.totalDelta += res.delta
p.totalBets++
if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') {
p.totalDelta += res.delta;
p.totalBets++;
if (res.result === "win" || res.result === "push" || res.result === "blackjack") {
const userDB = getUser.get(p.id);
if (userDB) {
const coins = userDB.coins;
try {
updateUserCoins.run({ id: p.id, coins: coins + hand.bet + res.delta });
updateUserCoins.run({
id: p.id,
coins: coins + hand.bet + res.delta,
});
insertLog.run({
id: `${p.id}-blackjack-${Date.now()}`,
user_id: p.id, target_user_id: null,
action: 'BLACKJACK_PAYOUT',
coins_amount: res.delta + hand.bet, user_new_amount: coins + hand.bet + res.delta,
user_id: p.id,
target_user_id: null,
action: "BLACKJACK_PAYOUT",
coins_amount: res.delta + hand.bet,
user_new_amount: coins + hand.bet + res.delta,
});
p.bank = coins + hand.bet + res.delta
p.bank = coins + hand.bet + res.delta;
} catch (e) {
console.log(e)
console.log(e);
}
}
}
@@ -283,25 +326,23 @@ export async function settleAll(room) {
hand.delta = res.delta;
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
const msg = await generalChannel.messages.fetch(p.msgId);
const updatedEmbed = new EmbedBuilder()
.setDescription(`<@${p.id}> joue au Blackjack.`)
.addFields(
{
name: `Gains`,
value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`,
inline: true
value: `**${p.totalDelta >= 0 ? "+" + p.totalDelta : p.totalDelta}** Flopos`,
inline: true,
},
{
name: `Mises jouées`,
value: `**${p.totalBets}**`,
inline: true
}
inline: true,
},
)
.setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245)
.setColor(p.totalDelta >= 0 ? 0x22a55b : 0xed4245)
.setTimestamp(new Date());
await msg.edit({ embeds: [updatedEmbed], components: [] });
} catch (e) {
@@ -334,8 +375,8 @@ export function applyAction(room, playerId, action) {
case "double": {
if (!canDouble(hand)) throw new Error("Cannot double now");
hand.doubled = true;
hand.bet*=2
p.currentBet+=hand.bet/2
hand.bet *= 2;
p.currentBet += hand.bet / 2;
hand.hasActed = true;
// The caller (routes) must also handle additional balance lock on the bet if using real coins
hand.cards.push(draw(room.shoe));
@@ -368,9 +409,9 @@ export function applyAction(room, playerId, action) {
surrendered: false,
hasActed: false,
bet: hand.bet,
}
};
p.currentBet *= 2
p.currentBet *= 2;
p.hands.splice(p.activeHand + 1, 0, newHand);

View File

@@ -1,10 +1,4 @@
import {
getUser,
getUserElo,
insertElos,
updateElo,
insertGame,
} from '../database/index.js';
import { getUser, getUserElo, insertElos, updateElo, insertGame } from "../database/index.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { client } from "../bot/client.js";
@@ -67,13 +61,17 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
const diff2 = finalP2Elo - p2CurrentElo;
const embed = new EmbedBuilder()
.setTitle(`FlopoRank - ${type}`)
.setDescription(`
**${user1.globalName || user1.username}** a ${diff1 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? '↗️' : '↘️'} **${finalP1Elo}**\n
**${user2.globalName || user2.username}** a ${diff2 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? '↗️' : '↘️'} **${finalP2Elo}**\n
`)
.setColor('#5865f2');
.setDescription(
`
**${user1.globalName || user1.username}** a ${diff1 > 0 ? "gagné" : "perdu"} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? "↗️" : "↘️"} **${finalP1Elo}**\n
**${user2.globalName || user2.username}** a ${diff2 > 0 ? "gagné" : "perdu"} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? "↗️" : "↘️"} **${finalP2Elo}**\n
`,
)
.setColor("#5865f2");
await generalChannel.send({ embeds: [embed] });
} catch (e) { console.error(`Failed to post elo update message`, e); }
} catch (e) {
console.error(`Failed to post elo update message`, e);
}
// --- 4. Update Database ---
updateElo.run({ id: p1Id, elo: finalP1Elo });
@@ -108,7 +106,7 @@ export async function pokerEloHandler(room) {
if (playerIds.length < 2) return; // Not enough players to calculate Elo
// Fetch all players' Elo data at once
const dbPlayers = playerIds.map(id => {
const dbPlayers = playerIds.map((id) => {
const user = getUser.get(id);
const elo = getUserElo.get({ id })?.elo || 1000;
return { ...user, elo };
@@ -120,7 +118,7 @@ export async function pokerEloHandler(room) {
const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount;
dbPlayers.forEach(player => {
dbPlayers.forEach((player) => {
// Expected score is the chance of winning against an "average" player from the field
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
@@ -139,7 +137,9 @@ export async function pokerEloHandler(room) {
const newElo = Math.max(100, Math.round(player.elo + eloChange));
if (!isNaN(newElo)) {
console.log(`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`);
console.log(
`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`,
);
updateElo.run({ id: player.id, elo: newElo });
insertGame.run({
@@ -152,7 +152,7 @@ export async function pokerEloHandler(room) {
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
p1_new_elo: newElo,
p2_new_elo: null,
type: 'POKER_ROUND',
type: "POKER_ROUND",
timestamp: Date.now(),
});
} else {

View File

@@ -5,10 +5,12 @@ import {
getAllSkins,
insertSOTD,
clearSOTDStats,
getAllSOTDStats, deleteSOTD, insertGame,
} from '../database/index.js';
import { messagesTimestamps, activeSlowmodes, skins } from './state.js';
import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js';
getAllSOTDStats,
deleteSOTD,
insertGame,
} from "../database/index.js";
import { messagesTimestamps, activeSlowmodes, skins } from "./state.js";
import { deal, createSeededRNG, seededShuffle, createDeck } from "./solitaire.js";
/**
* Handles awarding points (coins) to users for their message activity.
@@ -26,7 +28,7 @@ export async function channelPointsHandler(message) {
}
// Ignore short messages or commands that might be spammed
if (message.content.length < 3 || message.content.startsWith('?')) {
if (message.content.length < 3 || message.content.startsWith("?")) {
return false;
}
@@ -34,7 +36,7 @@ export async function channelPointsHandler(message) {
const userTimestamps = messagesTimestamps.get(author.id) || [];
// Filter out timestamps older than 15 minutes (900,000 ms)
const recentTimestamps = userTimestamps.filter(ts => now - ts < 900000);
const recentTimestamps = userTimestamps.filter((ts) => now - ts < 900000);
// If the user has already sent 10 messages in the last 15 mins, do nothing
if (recentTimestamps.length >= 10) {
@@ -57,7 +59,7 @@ export async function channelPointsHandler(message) {
insertLog.run({
id: `${author.id}-${now}`,
user_id: author.id,
action: 'AUTO_COINS',
action: "AUTO_COINS",
target_user_id: null,
coins_amount: coinsToAdd,
user_new_amount: newCoinTotal,
@@ -89,7 +91,7 @@ export async function slowmodesHandler(message) {
}
// Check if the user is messaging too quickly (less than 1 minute between messages)
if (authorSlowmode.lastMessage && (now - authorSlowmode.lastMessage < 60 * 1000)) {
if (authorSlowmode.lastMessage && now - authorSlowmode.lastMessage < 60 * 1000) {
try {
await message.delete();
console.log(`Deleted a message from slowmoded user: ${author.username}`);
@@ -112,12 +114,12 @@ export async function slowmodesHandler(message) {
*/
export function randomSkinPrice() {
const dbSkins = getAllSkins.all();
if (dbSkins.length === 0) return '0.00';
if (dbSkins.length === 0) return "0.00";
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
if (!randomSkinData) return '0.00';
if (!randomSkinData) return "0.00";
// Generate random level and chroma
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
@@ -128,8 +130,8 @@ export function randomSkinPrice() {
// Calculate price based on these random values
let result = parseFloat(randomDbSkin.basePrice);
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
result *= (1 + (randomChroma / 4));
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= 1 + randomChroma / 4;
return result.toFixed(0);
}
@@ -139,7 +141,7 @@ export function randomSkinPrice() {
* This function clears previous stats, awards the winner, and generates a new daily seed.
*/
export function initTodaysSOTD() {
console.log('Initializing new Solitaire of the Day...');
console.log("Initializing new Solitaire of the Day...");
// 1. Award previous day's winner
const rankings = getAllSOTDStats.all();
@@ -155,11 +157,13 @@ export function initTodaysSOTD() {
id: `${winnerId}-sotd-win-${Date.now()}`,
target_user_id: null,
user_id: winnerId,
action: 'SOTD_FIRST_PLACE',
action: "SOTD_FIRST_PLACE",
coins_amount: reward,
user_new_amount: newCoinTotal,
});
console.log(`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`);
console.log(
`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`,
);
insertGame.run({
id: `${winnerId}-${Date.now()}`,
p1: winnerId,
@@ -170,7 +174,7 @@ export function initTodaysSOTD() {
p2_elo: null,
p1_new_elo: winnerUser.elo,
p2_new_elo: null,
type: 'SOTD',
type: "SOTD",
timestamp: Date.now(),
});
}
@@ -180,7 +184,7 @@ export function initTodaysSOTD() {
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
let numericSeed = 0;
for (let i = 0; i < newRandomSeed.length; i++) {
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF;
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xffffffff;
}
const rng = createSeededRNG(numericSeed);

View File

@@ -1,12 +1,60 @@
import pkg from 'pokersolver';
import pkg from "pokersolver";
const { Hand } = pkg;
// An array of all 52 standard playing cards.
export const initialCards = [
'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd',
'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks',
'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc',
'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh',
"Ad",
"2d",
"3d",
"4d",
"5d",
"6d",
"7d",
"8d",
"9d",
"Td",
"Jd",
"Qd",
"Kd",
"As",
"2s",
"3s",
"4s",
"5s",
"6s",
"7s",
"8s",
"9s",
"Ts",
"Js",
"Qs",
"Ks",
"Ac",
"2c",
"3c",
"4c",
"5c",
"6c",
"7c",
"8c",
"9c",
"Tc",
"Jc",
"Qc",
"Kc",
"Ah",
"2h",
"3h",
"4h",
"5h",
"6h",
"7h",
"8h",
"9h",
"Th",
"Jh",
"Qh",
"Kh",
];
/**
@@ -70,30 +118,44 @@ export function checkEndOfBettingRound(room) {
// --- Scenario 1: Only one player left (everyone else folded) ---
if (activePlayers.length === 1) {
return { endRound: true, winner: activePlayers[0].id, nextPhase: 'showdown' };
return {
endRound: true,
winner: activePlayers[0].id,
nextPhase: "showdown",
};
}
// --- Scenario 2: All remaining players are all-in ---
// The hand goes immediately to a "progressive showdown".
const allInPlayers = activePlayers.filter(p => p.allin);
const allInPlayers = activePlayers.filter((p) => p.allin);
if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) {
return { endRound: true, winner: null, nextPhase: 'progressive-showdown' };
return { endRound: true, winner: null, nextPhase: "progressive-showdown" };
}
// --- Scenario 3: All active players have acted and bets are equal ---
const allBetsMatched = activePlayers.every(p =>
const allBetsMatched = activePlayers.every(
(p) =>
p.allin || // Player is all-in
(p.bet === room.highest_bet && p.last_played_turn === room.current_turn) // Or their bet matches the highest and they've acted this turn
(p.bet === room.highest_bet && p.last_played_turn === room.current_turn), // Or their bet matches the highest and they've acted this turn
);
if (allBetsMatched) {
let nextPhase;
switch (room.current_turn) {
case 0: nextPhase = 'flop'; break;
case 1: nextPhase = 'turn'; break;
case 2: nextPhase = 'river'; break;
case 3: nextPhase = 'showdown'; break;
default: nextPhase = null; // Should not happen
case 0:
nextPhase = "flop";
break;
case 1:
nextPhase = "turn";
break;
case 2:
nextPhase = "river";
break;
case 3:
nextPhase = "showdown";
break;
default:
nextPhase = null; // Should not happen
}
return { endRound: true, winner: null, nextPhase: nextPhase };
}
@@ -109,10 +171,10 @@ export function checkEndOfBettingRound(room) {
*/
export function checkRoomWinners(room) {
const communityCards = room.tapis;
const activePlayers = Object.values(room.players).filter(p => !p.folded);
const activePlayers = Object.values(room.players).filter((p) => !p.folded);
// Solve each player's hand to find the best possible 5-card combination
const playerSolutions = activePlayers.map(player => ({
const playerSolutions = activePlayers.map((player) => ({
id: player.id,
solution: Hand.solve([...communityCards, ...player.hand]),
}));
@@ -120,14 +182,17 @@ export function checkRoomWinners(room) {
if (playerSolutions.length === 0) return [];
// Use pokersolver's `Hand.winners()` to find the best hand(s)
const winningSolutions = Hand.winners(playerSolutions.map(ps => ps.solution));
const winningSolutions = Hand.winners(playerSolutions.map((ps) => ps.solution));
// Find the player IDs that correspond to the winning hand solutions
const winnerIds = [];
for (const winningHand of winningSolutions) {
for (const playerSol of playerSolutions) {
// Compare description and card pool to uniquely identify the hand
if (playerSol.solution.descr === winningHand.descr && playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()) {
if (
playerSol.solution.descr === winningHand.descr &&
playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()
) {
if (!winnerIds.includes(playerSol.id)) {
winnerIds.push(playerSol.id);
}

View File

@@ -2,8 +2,8 @@
import { sleep } from "openai/core";
import { emitSolitaireUpdate, emitUpdate } from "../server/socket.js";
const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
const SUITS = ["h", "d", "s", "c"]; // Hearts, Diamonds, Spades, Clubs
const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
// --- Helper Functions for Card Logic ---
@@ -13,11 +13,11 @@ const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
* @returns {number} The numeric value (Ace=1, King=13).
*/
function getRankValue(rank) {
if (rank === 'A') return 1;
if (rank === 'T') return 10;
if (rank === 'J') return 11;
if (rank === 'Q') return 12;
if (rank === 'K') return 13;
if (rank === "A") return 1;
if (rank === "T") return 10;
if (rank === "J") return 11;
if (rank === "Q") return 12;
if (rank === "K") return 13;
return parseInt(rank, 10);
}
@@ -27,10 +27,9 @@ function getRankValue(rank) {
* @returns {string} 'red' or 'black'.
*/
function getCardColor(suit) {
return (suit === 'h' || suit === 'd') ? 'red' : 'black';
return suit === "h" || suit === "d" ? "red" : "black";
}
// --- Core Game Logic Functions ---
/**
@@ -72,10 +71,10 @@ export function shuffle(array) {
*/
export function createSeededRNG(seed) {
return function () {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
let t = (seed += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
@@ -119,7 +118,7 @@ export function deal(deck) {
}
// Flip the top card of each tableau pile
gameState.tableauPiles.forEach(pile => {
gameState.tableauPiles.forEach((pile) => {
if (pile.length > 0) {
pile[pile.length - 1].faceUp = true;
}
@@ -142,9 +141,9 @@ export function isValidMove(gameState, moveData) {
// --- Get Source Pile and Card ---
let sourcePile;
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
else return false; // Invalid source type
const sourceCard = sourcePile?.[sourceCardIndex];
@@ -153,13 +152,13 @@ export function isValidMove(gameState, moveData) {
}
// --- Validate Move TO a Tableau Pile ---
if (destPileType === 'tableauPiles') {
if (destPileType === "tableauPiles") {
const destinationPile = gameState.tableauPiles[destPileIndex];
const topCard = destinationPile[destinationPile.length - 1];
if (!topCard) {
// If the destination tableau is empty, only a King can be moved there.
return sourceCard.rank === 'K';
return sourceCard.rank === "K";
}
// Card must be opposite color and one rank lower than the destination top card.
@@ -171,7 +170,7 @@ export function isValidMove(gameState, moveData) {
}
// --- Validate Move TO a Foundation Pile ---
if (destPileType === 'foundationPiles') {
if (destPileType === "foundationPiles") {
// You can only move one card at a time to a foundation pile.
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
if (stackBeingMoved.length > 1) return false;
@@ -181,7 +180,7 @@ export function isValidMove(gameState, moveData) {
if (!topCard) {
// If the foundation is empty, only an Ace of any suit can be moved there.
return sourceCard.rank === 'A';
return sourceCard.rank === "A";
}
// Card must be the same suit and one rank higher.
@@ -202,13 +201,13 @@ export function moveCard(gameState, moveData) {
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
let sourcePile;
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
let destPile;
if (destPileType === 'tableauPiles') destPile = gameState.tableauPiles[destPileIndex];
else if (destPileType === 'foundationPiles') destPile = gameState.foundationPiles[destPileIndex];
if (destPileType === "tableauPiles") destPile = gameState.tableauPiles[destPileIndex];
else if (destPileType === "foundationPiles") destPile = gameState.foundationPiles[destPileIndex];
// Cut the entire stack of cards to be moved from the source pile.
const cardsToMove = sourcePile.splice(sourceCardIndex);
@@ -216,7 +215,7 @@ export function moveCard(gameState, moveData) {
destPile.push(...cardsToMove);
const histMove = {
move: 'move',
move: "move",
sourcePileType: sourcePileType,
sourcePileIndex: sourcePileIndex,
sourceCardIndex: sourceCardIndex,
@@ -224,16 +223,16 @@ export function moveCard(gameState, moveData) {
destPileIndex: destPileIndex,
cardsMoved: cardsToMove,
cardWasFlipped: false,
points: destPileType === 'foundationPiles' ? 11 : 1 // Points for moving to foundation
}
points: destPileType === "foundationPiles" ? 11 : 1, // Points for moving to foundation
};
// If the source was a tableau pile and there are cards left, flip the new top card.
if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) {
if (sourcePileType === "tableauPiles" && sourcePile.length > 0) {
sourcePile[sourcePile.length - 1].faceUp = true;
histMove.cardWasFlipped = true;
}
gameState.hist.push(histMove)
gameState.hist.push(histMove);
}
/**
@@ -246,23 +245,23 @@ export function drawCard(gameState) {
card.faceUp = true;
gameState.wastePile.push(card);
gameState.hist.push({
move: 'draw',
card: card
})
move: "draw",
card: card,
});
} else if (gameState.wastePile.length > 0) {
// When stock is empty, move the entire waste pile back to stock, face down.
gameState.stockPile = gameState.wastePile.reverse();
gameState.stockPile.forEach(card => (card.faceUp = false));
gameState.stockPile.forEach((card) => (card.faceUp = false));
gameState.wastePile = [];
gameState.hist.push({
move: 'draw-reset',
})
move: "draw-reset",
});
}
}
export function draw3Cards(gameState) {
if (gameState.stockPile.length > 0) {
let cards = []
let cards = [];
for (let i = 0; i < 3; i++) {
if (gameState.stockPile.length > 0) {
const card = gameState.stockPile.pop();
@@ -274,19 +273,18 @@ export function draw3Cards(gameState) {
}
}
gameState.hist.push({
move: 'draw-3',
move: "draw-3",
cards: cards,
})
});
} else if (gameState.wastePile.length > 0) {
// When stock is empty, move the entire waste pile back to stock, face down.
gameState.stockPile = gameState.wastePile.reverse();
gameState.stockPile.forEach(card => (card.faceUp = false));
gameState.stockPile.forEach((card) => (card.faceUp = false));
gameState.wastePile = [];
gameState.hist.push({
move: 'draw-reset',
})
move: "draw-reset",
});
}
}
/**
@@ -320,12 +318,12 @@ export function autoSolveMoves(userId, gameState) {
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
function canMoveToFoundation(card) {
let foundationPile = foundations.find(pile => pile[pile.length - 1]?.suit === card.suit);
let foundationPile = foundations.find((pile) => pile[pile.length - 1]?.suit === card.suit);
if (!foundationPile) {
foundationPile = foundations.find(pile => pile.length === 0);
foundationPile = foundations.find((pile) => pile.length === 0);
}
if (foundationPile.length === 0) {
return card.rank === 'A'; // Only Ace can be placed on empty foundation
return card.rank === "A"; // Only Ace can be placed on empty foundation
} else {
const topCard = foundationPile[foundationPile.length - 1];
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
@@ -341,28 +339,28 @@ export function autoSolveMoves(userId, gameState) {
if (column.length === 0) continue;
const card = column[column.length - 1]; // Top card of the tableau column
let foundationIndex = foundations.findIndex(pile => pile[pile.length - 1]?.suit === card.suit);
let foundationIndex = foundations.findIndex((pile) => pile[pile.length - 1]?.suit === card.suit);
if (foundationIndex === -1) {
foundationIndex = foundations.findIndex(pile => pile.length === 0);
foundationIndex = foundations.findIndex((pile) => pile.length === 0);
}
if (canMoveToFoundation(card)) {
let moveData = {
destPileIndex: foundationIndex,
destPileType: 'foundationPiles',
destPileType: "foundationPiles",
sourceCardIndex: column.length - 1,
sourcePileIndex: i,
sourcePileType: 'tableauPiles',
sourcePileType: "tableauPiles",
userId: userId,
}
tableau[i].pop()
foundations[foundationIndex].push(card)
};
tableau[i].pop();
foundations[foundationIndex].push(card);
//moveCard(gameState, moveData)
moves.push(moveData);
moved = true;
}
}
} while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0));
emitSolitaireUpdate(userId, moves)
} while (moved); //(foundations.reduce((acc, pile) => acc + pile.length, 0));
emitSolitaireUpdate(userId, moves);
}
/**
@@ -381,16 +379,16 @@ export function undoMove(gameState) {
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
switch (lastMove.move) {
case 'move':
case "move":
undoCardMove(gameState, lastMove);
break;
case 'draw':
case "draw":
undoDraw(gameState, lastMove);
break;
case 'draw-3':
case "draw-3":
undoDraw3(gameState, lastMove);
break;
case 'draw-reset':
case "draw-reset":
undoDrawReset(gameState, lastMove);
break;
default:
@@ -406,12 +404,13 @@ export function undoMove(gameState) {
// --- Helper functions for undoing specific moves ---
function undoCardMove(gameState, moveData) {
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = moveData;
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } =
moveData;
// 1. Find the destination pile (where the cards are NOW)
let currentPile;
if (destPileType === 'tableauPiles') currentPile = gameState.tableauPiles[destPileIndex];
else if (destPileType === 'foundationPiles') currentPile = gameState.foundationPiles[destPileIndex];
if (destPileType === "tableauPiles") currentPile = gameState.tableauPiles[destPileIndex];
else if (destPileType === "foundationPiles") currentPile = gameState.foundationPiles[destPileIndex];
// 2. Remove the moved cards from their current pile
// Using splice with a negative index removes from the end of the array
@@ -419,9 +418,9 @@ function undoCardMove(gameState, moveData) {
// 3. Find the original source pile
let originalPile;
if (sourcePileType === 'tableauPiles') originalPile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === 'wastePile') originalPile = gameState.wastePile;
else if (sourcePileType === 'foundationPiles') originalPile = gameState.foundationPiles[sourcePileIndex];
if (sourcePileType === "tableauPiles") originalPile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === "wastePile") originalPile = gameState.wastePile;
else if (sourcePileType === "foundationPiles") originalPile = gameState.foundationPiles[sourcePileIndex];
// 4. Put the cards back where they came from
// Using splice to insert the cards back at their original index
@@ -463,6 +462,6 @@ function undoDrawReset(gameState, moveData) {
// A 'draw-reset' means the waste pile was moved to the stock pile.
// To undo, move the stock pile back to the waste pile and flip cards face-up.
gameState.wastePile = gameState.stockPile.reverse();
gameState.wastePile.forEach(card => (card.faceUp = true));
gameState.wastePile.forEach((card) => (card.faceUp = true));
gameState.stockPile = [];
}

View File

@@ -44,7 +44,6 @@ export let activePredis = {};
// Format: { [userId]: { endAt, lastMessage } }
export let activeSlowmodes = {};
// --- Queues for Matchmaking ---
// Stores user IDs waiting to play Tic-Tac-Toe.
@@ -55,7 +54,6 @@ export let connect4Queue = [];
export let queueMessagesEndpoints = [];
// --- Rate Limiting and Caching ---
// Tracks message timestamps for the channel points system, keyed by user ID.

View File

@@ -4,19 +4,19 @@ export const C4_COLS = 7;
// A predefined list of choices for the /timeout command's duration option.
const TimesChoices = [
{ name: '1 minute', value: 60 },
{ name: '5 minutes', value: 300 },
{ name: '10 minutes', value: 600 },
{ name: '15 minutes', value: 900 },
{ name: '30 minutes', value: 1800 },
{ name: '1 heure', value: 3600 },
{ name: '2 heures', value: 7200 },
{ name: '3 heures', value: 10800 },
{ name: '6 heures', value: 21600 },
{ name: '9 heures', value: 32400 },
{ name: '12 heures', value: 43200 },
{ name: '16 heures', value: 57600 },
{ name: '1 jour', value: 86400 },
{ name: "1 minute", value: 60 },
{ name: "5 minutes", value: 300 },
{ name: "10 minutes", value: 600 },
{ name: "15 minutes", value: 900 },
{ name: "30 minutes", value: 1800 },
{ name: "1 heure", value: 3600 },
{ name: "2 heures", value: 7200 },
{ name: "3 heures", value: 10800 },
{ name: "6 heures", value: 21600 },
{ name: "9 heures", value: 32400 },
{ name: "12 heures", value: 43200 },
{ name: "16 heures", value: 57600 },
{ name: "1 jour", value: 86400 },
];
/**
@@ -27,7 +27,6 @@ export function getTimesChoices() {
return TimesChoices;
}
// --- Connect 4 Logic ---
/**
@@ -35,7 +34,9 @@ export function getTimesChoices() {
* @returns {Array<Array<null>>} A 2D array representing the board.
*/
export function createConnect4Board() {
return Array(C4_ROWS).fill(null).map(() => Array(C4_COLS).fill(null));
return Array(C4_ROWS)
.fill(null)
.map(() => Array(C4_COLS).fill(null));
}
/**
@@ -48,8 +49,21 @@ export function checkConnect4Win(board, player) {
// Check horizontal
for (let r = 0; r < C4_ROWS; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) {
if (board[r][c] === player && board[r][c+1] === player && board[r][c+2] === player && board[r][c+3] === player) {
return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] };
if (
board[r][c] === player &&
board[r][c + 1] === player &&
board[r][c + 2] === player &&
board[r][c + 3] === player
) {
return {
win: true,
pieces: [
{ row: r, col: c },
{ row: r, col: c + 1 },
{ row: r, col: c + 2 },
{ row: r, col: c + 3 },
],
};
}
}
}
@@ -57,8 +71,21 @@ export function checkConnect4Win(board, player) {
// Check vertical
for (let r = 0; r <= C4_ROWS - 4; r++) {
for (let c = 0; c < C4_COLS; c++) {
if (board[r][c] === player && board[r+1][c] === player && board[r+2][c] === player && board[r+3][c] === player) {
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] };
if (
board[r][c] === player &&
board[r + 1][c] === player &&
board[r + 2][c] === player &&
board[r + 3][c] === player
) {
return {
win: true,
pieces: [
{ row: r, col: c },
{ row: r + 1, col: c },
{ row: r + 2, col: c },
{ row: r + 3, col: c },
],
};
}
}
}
@@ -66,8 +93,21 @@ export function checkConnect4Win(board, player) {
// Check diagonal (down-right)
for (let r = 0; r <= C4_ROWS - 4; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) {
if (board[r][c] === player && board[r+1][c+1] === player && board[r+2][c+2] === player && board[r+3][c+3] === player) {
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c+1}, {row:r+2, col:c+2}, {row:r+3, col:c+3}] };
if (
board[r][c] === player &&
board[r + 1][c + 1] === player &&
board[r + 2][c + 2] === player &&
board[r + 3][c + 3] === player
) {
return {
win: true,
pieces: [
{ row: r, col: c },
{ row: r + 1, col: c + 1 },
{ row: r + 2, col: c + 2 },
{ row: r + 3, col: c + 3 },
],
};
}
}
}
@@ -75,8 +115,21 @@ export function checkConnect4Win(board, player) {
// Check diagonal (up-right)
for (let r = 3; r < C4_ROWS; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) {
if (board[r][c] === player && board[r-1][c+1] === player && board[r-2][c+2] === player && board[r-3][c+3] === player) {
return { win: true, pieces: [{row:r, col:c}, {row:r-1, col:c+1}, {row:r-2, col:c+2}, {row:r-3, col:c+3}] };
if (
board[r][c] === player &&
board[r - 1][c + 1] === player &&
board[r - 2][c + 2] === player &&
board[r - 3][c + 3] === player
) {
return {
win: true,
pieces: [
{ row: r, col: c },
{ row: r - 1, col: c + 1 },
{ row: r - 2, col: c + 2 },
{ row: r - 3, col: c + 3 },
],
};
}
}
}
@@ -91,7 +144,7 @@ export function checkConnect4Win(board, player) {
*/
export function checkConnect4Draw(board) {
// A draw occurs if the top row is completely full.
return board[0].every(cell => cell !== null);
return board[0].every((cell) => cell !== null);
}
/**
@@ -101,9 +154,9 @@ export function checkConnect4Draw(board) {
*/
export function formatConnect4BoardForDiscord(board) {
const symbols = {
'R': '🔴',
'Y': '🟡',
null: '⚪' // Using a white circle for empty slots
R: "🔴",
Y: "🟡",
null: "⚪", // Using a white circle for empty slots
};
return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n');
return board.map((row) => row.map((cell) => symbols[cell]).join("")).join("\n");
}

View File

@@ -1,34 +1,37 @@
import 'dotenv/config';
import express from 'express';
import { verifyKeyMiddleware } from 'discord-interactions';
import { handleInteraction } from '../bot/handlers/interactionCreate.js';
import { client } from '../bot/client.js';
import "dotenv/config";
import express from "express";
import { verifyKeyMiddleware } from "discord-interactions";
import { handleInteraction } from "../bot/handlers/interactionCreate.js";
import { client } from "../bot/client.js";
// Import route handlers
import { apiRoutes } from './routes/api.js';
import { pokerRoutes } from './routes/poker.js';
import { solitaireRoutes } from './routes/solitaire.js';
import { apiRoutes } from "./routes/api.js";
import { pokerRoutes } from "./routes/poker.js";
import { solitaireRoutes } from "./routes/solitaire.js";
import { getSocketIo } from "./socket.js";
import {erinyesRoutes} from "./routes/erinyes.js";
import { blackjackRoutes } from "./routes/blackjack.js";
import { marketRoutes } from "./routes/market.js";
// --- EXPRESS APP INITIALIZATION ---
const app = express();
const io = getSocketIo();
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;
// --- GLOBAL MIDDLEWARE ---
// CORS Middleware
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', FLAPI_URL);
res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires');
res.header("Access-Control-Allow-Origin", FLAPI_URL);
res.header(
"Access-Control-Allow-Headers",
"Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
);
next();
});
// --- PRIMARY DISCORD INTERACTION ENDPOINT ---
// This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.)
app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
// The actual logic is delegated to a dedicated handler for better organization
await handleInteraction(req, res, client);
});
@@ -37,24 +40,26 @@ app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re
app.use(express.json());
// --- STATIC ASSETS ---
app.use('/public', express.static('public'));
app.use("/public", express.static("public"));
// --- API ROUTES ---
// General API routes (users, polls, etc.)
app.use('/api', apiRoutes(client, io));
app.use("/api", apiRoutes(client, io));
// Poker-specific routes
app.use('/api/poker', pokerRoutes(client, io));
app.use("/api/poker", pokerRoutes(client, io));
// Solitaire-specific routes
app.use('/api/solitaire', solitaireRoutes(client, io));
app.use("/api/solitaire", solitaireRoutes(client, io));
app.use('/api/blackjack', blackjackRoutes(client, io));
// Blackjack-specific routes
app.use("/api/blackjack", blackjackRoutes(client, io));
// Market-specific routes
app.use("/api/market-place", marketRoutes(client, io));
// erinyes-specific routes
app.use('/api/erinyes', erinyesRoutes(client, io));
// app.use("/api/erinyes", erinyesRoutes(client, io));
export { app };

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,8 @@ import {
applyAction,
publicPlayerView,
handValue,
dealerShouldHit, draw
dealerShouldHit,
draw,
} from "../../game/blackjack.js";
// Optional: hook into your DB & Discord systems if available
@@ -32,11 +33,17 @@ export function blackjackRoutes(io) {
hitSoft17: false, // S17 (dealer stands on soft 17) if false
blackjackPayout: 1.5, // 3:2
cutCardRatio: 0.25,
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 7000 },
animation: { dealerDrawMs: 1000 }
phaseDurations: {
bettingMs: 10000,
dealMs: 2000,
playMsPerPlayer: 20000,
revealMs: 1000,
payoutMs: 7000,
},
animation: { dealerDrawMs: 1000 },
});
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
let animatingDealer = false;
async function runDealerAnimation() {
@@ -60,7 +67,7 @@ export function blackjackRoutes(io) {
settleAll(room);
room.status = "payout";
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
emitUpdate("payout", snapshot(room))
emitUpdate("payout", snapshot(room));
animatingDealer = false;
}
@@ -100,7 +107,10 @@ export function blackjackRoutes(io) {
minBet: r.minBet,
maxBet: r.maxBet,
settings: r.settings,
dealer: { cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total },
dealer: {
cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards,
total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total,
},
players: Object.values(r.players).map(publicPlayerView),
shoeCount: r.shoe.length,
};
@@ -124,7 +134,17 @@ export function blackjackRoutes(io) {
bank,
currentBet: 0,
inRound: false,
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 }],
hands: [
{
cards: [],
stood: false,
busted: false,
doubled: false,
surrendered: false,
hasActed: false,
bet: 0,
},
],
activeHand: 0,
joined_at: Date.now(),
msgId: null,
@@ -134,24 +154,22 @@ export function blackjackRoutes(io) {
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
const embed = new EmbedBuilder()
.setDescription(`<@${userId}> joue au Blackjack`)
.addFields(
{
name: `Gains`,
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
inline: true
value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
inline: true,
},
{
name: `Mises jouées`,
value: `**${room.players[userId].totalBets}**`,
inline: true
}
inline: true,
},
)
.setColor('#5865f2')
.setColor("#5865f2")
.setTimestamp(new Date());
const msg = await generalChannel.send({ embeds: [embed] });
@@ -170,25 +188,23 @@ export function blackjackRoutes(io) {
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
const updatedEmbed = new EmbedBuilder()
.setDescription(`<@${userId}> a quitté la table de Blackjack.`)
.addFields(
{
name: `Gains`,
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
inline: true
value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
inline: true,
},
{
name: `Mises jouées`,
value: `**${room.players[userId].totalBets}**`,
inline: true
}
inline: true,
},
)
.setColor(room.players[userId].totalDelta >= 0 ? 0x22A55B : 0xED4245)
.setColor(room.players[userId].totalDelta >= 0 ? 0x22a55b : 0xed4245)
.setTimestamp(new Date());
await msg.edit({ embeds: [updatedEmbed], components: [] });
} catch (e) {
@@ -223,9 +239,11 @@ export function blackjackRoutes(io) {
updateUserCoins.run({ id: userId, coins: coins - bet });
insertLog.run({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'BLACKJACK_BET',
coins_amount: -bet, user_new_amount: coins - bet,
user_id: userId,
target_user_id: null,
action: "BLACKJACK_BET",
coins_amount: -bet,
user_new_amount: coins - bet,
});
p.bank = coins - bet;
}
@@ -253,9 +271,11 @@ export function blackjackRoutes(io) {
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
insertLog.run({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'BLACKJACK_DOUBLE',
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
user_id: userId,
target_user_id: null,
action: "BLACKJACK_DOUBLE",
coins_amount: -hand.bet,
user_new_amount: coins - hand.bet,
});
p.bank = coins - hand.bet;
// effective bet size is handled in settlement via hand.doubled flag
@@ -269,9 +289,11 @@ export function blackjackRoutes(io) {
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
insertLog.run({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'BLACKJACK_SPLIT',
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
user_id: userId,
target_user_id: null,
action: "BLACKJACK_SPLIT",
coins_amount: -hand.bet,
user_new_amount: coins - hand.bet,
});
p.bank = coins - hand.bet;
// effective bet size is handled in settlement via hand.doubled flag
@@ -293,7 +315,7 @@ export function blackjackRoutes(io) {
const now = Date.now();
if (room.status === "betting" && now >= room.phase_ends_at) {
const hasBets = Object.values(room.players).some(p => p.currentBet >= room.minBet);
const hasBets = Object.values(room.players).some((p) => p.currentBet >= room.minBet);
if (!hasBets) {
// Extend betting window if no one bet
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;

View File

@@ -1,5 +1,5 @@
import express from "express";
import { v4 as uuidv4 } from 'uuid';
import { v4 as uuidv4 } from "uuid";
import { erinyesRooms } from "../../game/state.js";
import { socketEmit } from "../socket.js";
@@ -12,43 +12,45 @@ const router = express.Router();
* @returns {object} The configured Express router.
*/
export function erinyesRoutes(client, io) {
// --- Router Management Endpoints
router.get('/', (req, res) => {
res.status(200).json({ rooms: erinyesRooms })
})
router.get("/", (req, res) => {
res.status(200).json({ rooms: erinyesRooms });
});
router.get('/:id', (req, res) => {
router.get("/:id", (req, res) => {
const room = erinyesRooms[req.params.id];
if (room) {
res.status(200).json({ room });
} else {
res.status(404).json({ message: 'Room not found.' });
res.status(404).json({ message: "Room not found." });
}
})
});
router.post('/create', async (req, res) => {
router.post("/create", async (req, res) => {
const { creatorId } = req.body;
if (!creatorId) return res.status(404).json({ message: 'Creator ID is required.' });
if (!creatorId) return res.status(404).json({ message: "Creator ID is required." });
if (Object.values(erinyesRooms).some(room => creatorId === room.host_id || room.players[creatorId])) {
res.status(404).json({ message: 'You are already in a room.' });
if (Object.values(erinyesRooms).some((room) => creatorId === room.host_id || room.players[creatorId])) {
res.status(404).json({ message: "You are already in a room." });
}
const creator = await client.users.fetch(creatorId);
const id = uuidv4()
const id = uuidv4();
createRoom({
host_id: creatorId,
host_name: creator.globalName,
game_rules: {}, // Specific game rules
roles: [], // Every role in the game
})
});
await socketEmit('erinyes-update', { room: erinyesRooms[id], type: 'room-created' });
await socketEmit("erinyes-update", {
room: erinyesRooms[id],
type: "room-created",
});
res.status(200).json({ room: id });
})
});
return router;
}
@@ -66,8 +68,8 @@ function createRoom(config) {
game_rules: createGameRules(config.game_rules),
roles: config.roles,
roles_rules: createRolesRules(config.roles),
bonuses: {}
}
bonuses: {},
};
}
function createGameRules(config) {
@@ -78,11 +80,11 @@ function createGameRules(config) {
}
function createRolesRules(roles) {
const roles_rules = {}
const roles_rules = {};
roles.forEach(role => {
roles.forEach((role) => {
switch (role) {
case 'erynie':
case "erynie":
roles_rules[role] = {
//...
};
@@ -94,7 +96,7 @@ function createRolesRules(roles) {
};
break;
}
})
});
return roles_rules;
}

View File

@@ -0,0 +1,73 @@
import express from "express";
// --- Database Imports ---
// --- Game State Imports ---
// --- Utility and API Imports ---
// --- Discord.js Builder Imports ---
import { ButtonStyle } from "discord.js";
import { getMarketOfferById, getMarketOffers, getOfferBids } from "../../database/index.js";
// Create a new router instance
const router = express.Router();
/**
* Factory function to create and configure the market routes.
* @param {object} client - The Discord.js client instance.
* @param {object} io - The Socket.IO server instance.
* @returns {object} The configured Express router.
*/
export function marketRoutes(client, io) {
router.get("/offers", async (req, res) => {
try {
const offers = getMarketOffers.all();
res.status(200).send({ offers });
} catch (e) {
res.status(500).send({ error: e });
}
});
router.get("/offers/:id", async (req, res) => {
try {
const offer = getMarketOfferById.get(req.params.id);
if (offer) {
res.status(200).send({ offer });
} else {
res.status(404).send({ error: "Offer not found" });
}
} catch (e) {
res.status(500).send({ error: e });
}
});
router.get("/offers/:id/bids", async (req, res) => {
try {
// Placeholder for fetching bids logic
const bids = getOfferBids.get(req.params.id);
res.status(200).send({ bids });
} catch (e) {
res.status(500).send({ error: e });
}
});
router.post("/place-offer", async (req, res) => {
try {
// Placeholder for placing an offer logic
// Extract data from req.body and process accordingly
res.status(200).send({ message: "Offer placed successfully" });
} catch (e) {
res.status(500).send({ error: e });
}
});
router.post("/offers/:id/place-bid", async (req, res) => {
try {
// Placeholder for placing a bid logic
// Extract data from req.body and process accordingly
res.status(200).send({ message: "Bid placed successfully" });
} catch (e) {
res.status(500).send({ error: e });
}
});
return router;
}

View File

@@ -1,13 +1,19 @@
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import { uniqueNamesGenerator, adjectives } from 'unique-names-generator';
import pkg from 'pokersolver';
import express from "express";
import { v4 as uuidv4 } from "uuid";
import { uniqueNamesGenerator, adjectives } from "unique-names-generator";
import pkg from "pokersolver";
const { Hand } = pkg;
import { pokerRooms } from '../../game/state.js';
import { initialShuffledCards, getFirstActivePlayerAfterDealer, getNextActivePlayer, checkEndOfBettingRound, checkRoomWinners } from '../../game/poker.js';
import { pokerEloHandler } from '../../game/elo.js';
import { getUser, updateUserCoins, insertLog } from '../../database/index.js';
import { pokerRooms } from "../../game/state.js";
import {
initialShuffledCards,
getFirstActivePlayerAfterDealer,
getNextActivePlayer,
checkEndOfBettingRound,
checkRoomWinners,
} from "../../game/poker.js";
import { pokerEloHandler } from "../../game/elo.js";
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
import { sleep } from "openai/core";
import { client } from "../../bot/client.js";
import { emitPokerToast, emitPokerUpdate } from "../socket.js";
@@ -23,218 +29,264 @@ const router = express.Router();
* @returns {object} The configured Express router.
*/
export function pokerRoutes(client, io) {
// --- Room Management Endpoints ---
router.get('/', (req, res) => {
router.get("/", (req, res) => {
res.status(200).json({ rooms: pokerRooms });
});
router.get('/:id', (req, res) => {
router.get("/:id", (req, res) => {
const room = pokerRooms[req.params.id];
if (room) {
res.status(200).json({ room });
} else {
res.status(404).json({ message: 'Poker room not found.' });
res.status(404).json({ message: "Poker room not found." });
}
});
router.post('/create', async (req, res) => {
router.post("/create", async (req, res) => {
const { creatorId, minBet, fakeMoney } = req.body;
if (!creatorId) return res.status(400).json({ message: 'Creator ID is required.' });
if (!creatorId) return res.status(400).json({ message: "Creator ID is required." });
if (Object.values(pokerRooms).some(room => room.host_id === creatorId || room.players[creatorId])) {
return res.status(403).json({ message: 'You are already in a poker room.' });
if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) {
return res.status(403).json({ message: "You are already in a poker room." });
}
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const creator = await client.users.fetch(creatorId);
const id = uuidv4();
const name = uniqueNamesGenerator({ dictionaries: [adjectives, ['Poker']], separator: ' ', style: 'capital' });
const name = uniqueNamesGenerator({
dictionaries: [adjectives, ["Poker"]],
separator: " ",
style: "capital",
});
pokerRooms[id] = {
id, host_id: creatorId, host_name: creator.globalName || creator.username,
name, created_at: Date.now(), last_move_at: null,
players: {}, queue: {}, afk: {}, pioche: initialShuffledCards(), tapis: [],
dealer: null, sb: null, bb: null, highest_bet: 0, current_player: null,
current_turn: null, playing: false, winners: [], waiting_for_restart: false, fakeMoney: fakeMoney,
id,
host_id: creatorId,
host_name: creator.globalName || creator.username,
name,
created_at: Date.now(),
last_move_at: null,
players: {},
queue: {},
afk: {},
pioche: initialShuffledCards(),
tapis: [],
dealer: null,
sb: null,
bb: null,
highest_bet: 0,
current_player: null,
current_turn: null,
playing: false,
winners: [],
waiting_for_restart: false,
fakeMoney: fakeMoney,
minBet: minBet,
};
await joinRoom(id, creatorId, io); // Auto-join the creator
await emitPokerUpdate({ room: pokerRooms[id], type: 'room-created' });
await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" });
try {
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
const embed = new EmbedBuilder()
.setTitle('Flopoker 🃏')
.setTitle("Flopoker 🃏")
.setDescription(`<@${creatorId}> a créé une table de poker`)
.addFields(
{ name: `Nom`, value: `**${name}**`, inline: true },
{ name: `${fakeMoney ? 'Mise initiale' : 'Prix d\'entrée'}`, value: `**${formatAmount(minBet)}** 🪙`, inline: true },
{ name: `Fake Money`, value: `${fakeMoney ? '**Oui** ✅' : '**Non** ❌'}`, inline: true },
{
name: `${fakeMoney ? "Mise initiale" : "Prix d'entrée"}`,
value: `**${formatAmount(minBet)}** 🪙`,
inline: true,
},
{
name: `Fake Money`,
value: `${fakeMoney ? "**Oui** ✅" : "**Non** ❌"}`,
inline: true,
},
)
.setColor('#5865f2')
.setColor("#5865f2")
.setTimestamp(new Date());
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel(`Rejoindre la table ${name}`)
.setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
.setStyle(ButtonStyle.Link)
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
.setStyle(ButtonStyle.Link),
);
await generalChannel.send({ embeds: [embed], components: [row] });
} catch (e) {
console.log(e)
console.log(e);
}
res.status(201).json({ roomId: id });
});
router.post('/join', async (req, res) => {
router.post("/join", async (req, res) => {
const { userId, roomId } = req.body;
if (!userId || !roomId) return res.status(400).json({ message: 'User ID and Room ID are required.' });
if (!pokerRooms[roomId]) return res.status(404).json({ message: 'Room not found.' });
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.' });
if (!userId || !roomId) return res.status(400).json({ message: "User ID and Room ID are required." });
if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." });
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." });
}
if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > (getUser.get(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." });
}
await joinRoom(roomId, userId, io);
res.status(200).json({ message: 'Successfully joined.' });
res.status(200).json({ message: "Successfully joined." });
});
router.post('/accept', async (req, res) => {
router.post("/accept", async (req, res) => {
const { hostId, playerId, roomId } = req.body;
const room = pokerRooms[roomId];
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
return res.status(403).json({ message: 'Unauthorized or player not in queue.' });
return res.status(403).json({ message: "Unauthorized or player not in queue." });
}
if (!room.fakeMoney) {
const userDB = getUser.get(playerId);
if (userDB) {
updateUserCoins.run({ id: playerId, coins: userDB.coins - room.minBet });
updateUserCoins.run({
id: playerId,
coins: userDB.coins - room.minBet,
});
insertLog.run({
id: `${playerId}-poker-${Date.now()}`,
user_id: playerId, target_user_id: null,
action: 'POKER_JOIN',
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
})
user_id: playerId,
target_user_id: null,
action: "POKER_JOIN",
coins_amount: -room.minBet,
user_new_amount: userDB.coins - room.minBet,
});
}
}
room.players[playerId] = room.queue[playerId];
delete room.queue[playerId];
await emitPokerUpdate({ room: room, type: 'player-accepted' });
res.status(200).json({ message: 'Player accepted.' });
await emitPokerUpdate({ room: room, type: "player-accepted" });
res.status(200).json({ message: "Player accepted." });
});
router.post('/leave', async (req, res) => {
const { userId, roomId } = req.body
router.post("/leave", async (req, res) => {
const { userId, roomId } = req.body;
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId]
if (
pokerRooms[roomId].playing &&
pokerRooms[roomId].current_turn !== null &&
pokerRooms[roomId].current_turn !== 4
) {
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId];
try {
pokerRooms[roomId].players[userId].folded = true
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn
pokerRooms[roomId].players[userId].folded = true;
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn;
if (pokerRooms[roomId].current_player === userId) {
await checkRoundCompletion(pokerRooms[roomId], io);
}
} catch (e) {
console.log(e)
console.log(e);
}
await emitPokerUpdate({ type: 'player-afk' });
return res.status(200)
await emitPokerUpdate({ type: "player-afk" });
return res.status(200);
}
try {
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
delete pokerRooms[roomId].players[userId]
updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
);
delete pokerRooms[roomId].players[userId];
if (userId === pokerRooms[roomId].host_id) {
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
if (!newHostId) {
delete pokerRooms[roomId]
delete pokerRooms[roomId];
} else {
pokerRooms[roomId].host_id = newHostId
pokerRooms[roomId].host_id = newHostId;
}
}
} catch (e) {
console.log(e)
console.log(e);
}
await emitPokerUpdate({ type: 'player-left' });
return res.status(200)
await emitPokerUpdate({ type: "player-left" });
return res.status(200);
});
router.post('/kick', async (req, res) => {
const { commandUserId, userId, roomId } = req.body
router.post("/kick", async (req, res) => {
const { commandUserId, userId, roomId } = req.body;
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: 'Joueur introuvable' })
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: 'Seul l\'host peut kick' })
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" });
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: "Seul l'host peut kick" });
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
return res.status(403).send({ message: 'Playing' })
if (
pokerRooms[roomId].playing &&
pokerRooms[roomId].current_turn !== null &&
pokerRooms[roomId].current_turn !== 4
) {
return res.status(403).send({ message: "Playing" });
}
try {
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
delete pokerRooms[roomId].players[userId]
updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
);
delete pokerRooms[roomId].players[userId];
if (userId === pokerRooms[roomId].host_id) {
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
if (!newHostId) {
delete pokerRooms[roomId]
delete pokerRooms[roomId];
} else {
pokerRooms[roomId].host_id = newHostId
pokerRooms[roomId].host_id = newHostId;
}
}
} catch (e) {
console.log(e)
console.log(e);
}
await emitPokerUpdate({ type: 'player-kicked' });
return res.status(200)
await emitPokerUpdate({ type: "player-kicked" });
return res.status(200);
});
// --- Game Action Endpoints ---
router.post('/start', async (req, res) => {
router.post("/start", async (req, res) => {
const { roomId } = req.body;
const room = pokerRooms[roomId];
if (!room) return res.status(404).json({ message: 'Room not found.' });
if (Object.keys(room.players).length < 2) return res.status(400).json({ message: 'Not enough players to start.' });
if (!room) return res.status(404).json({ message: "Room not found." });
if (Object.keys(room.players).length < 2) return res.status(400).json({ message: "Not enough players to start." });
await startNewHand(room, io);
res.status(200).json({ message: 'Game started.' });
res.status(200).json({ message: "Game started." });
});
// NEW: Endpoint to start the next hand
router.post('/next-hand', async (req, res) => {
router.post("/next-hand", async (req, res) => {
const { roomId } = req.body;
const room = pokerRooms[roomId];
if (!room || !room.waiting_for_restart) {
return res.status(400).json({ message: 'Not ready for the next hand.' });
return res.status(400).json({ message: "Not ready for the next hand." });
}
await startNewHand(room, io);
res.status(200).json({ message: 'Next hand started.' });
res.status(200).json({ message: "Next hand started." });
});
router.post('/action/:action', async (req, res) => {
router.post("/action/:action", async (req, res) => {
const { playerId, amount, roomId } = req.body;
const { action } = req.params;
const room = pokerRooms[roomId];
@@ -246,53 +298,54 @@ export function pokerRoutes(client, io) {
const player = room.players[playerId];
switch (action) {
case 'fold':
case "fold":
player.folded = true;
await emitPokerToast({
type: 'player-fold',
type: "player-fold",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
})
});
break;
case 'check':
if (player.bet < room.highest_bet) return res.status(400).json({ message: 'Cannot check.' });
case "check":
if (player.bet < room.highest_bet) return res.status(400).json({ message: "Cannot check." });
await emitPokerToast({
type: 'player-check',
type: "player-check",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
})
});
break;
case 'call':
case "call":
const callAmount = Math.min(room.highest_bet - player.bet, player.bank);
player.bank -= callAmount;
player.bet += callAmount;
if (player.bank === 0) player.allin = true;
await emitPokerToast({
type: 'player-call',
type: "player-call",
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
})
});
break;
case 'raise':
if (amount <= 0 || amount > player.bank || (player.bet + amount) <= room.highest_bet) {
return res.status(400).json({ message: 'Invalid raise amount.' });
case "raise":
if (amount <= 0 || amount > player.bank || player.bet + amount <= room.highest_bet) {
return res.status(400).json({ message: "Invalid raise amount." });
}
player.bank -= amount;
player.bet += amount;
if (player.bank === 0) player.allin = true;
room.highest_bet = player.bet;
await emitPokerToast({
type: 'player-raise',
type: "player-raise",
amount: amount,
playerId: player.id,
playerName: player.globalName,
roomId: room.id,
})
});
break;
default: return res.status(400).json({ message: 'Invalid action.' });
default:
return res.status(400).json({ message: "Invalid action." });
}
player.last_played_turn = room.current_turn;
@@ -303,7 +356,6 @@ export function pokerRoutes(client, io) {
return router;
}
// --- Helper Functions ---
async function joinRoom(roomId, userId, io) {
@@ -312,9 +364,16 @@ async function joinRoom(roomId, userId, io) {
const room = pokerRooms[roomId];
const playerObject = {
id: userId, globalName: user.globalName || user.username, avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
hand: [], bank: room.minBet, bet: 0, folded: false, allin: false,
last_played_turn: null, solve: null
id: userId,
globalName: user.globalName || user.username,
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
hand: [],
bank: room.minBet,
bet: 0,
folded: false,
allin: false,
last_played_turn: null,
solve: null,
};
if (room.playing) {
@@ -325,21 +384,23 @@ async function joinRoom(roomId, userId, io) {
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
insertLog.run({
id: `${userId}-poker-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'POKER_JOIN',
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
})
user_id: userId,
target_user_id: null,
action: "POKER_JOIN",
coins_amount: -room.minBet,
user_new_amount: userDB.coins - room.minBet,
});
}
}
await emitPokerUpdate({ room: room, type: 'player-joined' });
await emitPokerUpdate({ room: room, type: "player-joined" });
}
async function startNewHand(room, io) {
const playerIds = Object.keys(room.players);
if (playerIds.length < 2) {
room.playing = false; // Not enough players to continue
await emitPokerUpdate({ room: room, type: 'new-hand' });
await emitPokerUpdate({ room: room, type: "new-hand" });
return;
}
@@ -356,9 +417,12 @@ async function startNewHand(room, io) {
const oldDealerIndex = playerIds.indexOf(room.dealer);
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
Object.values(room.players).forEach(p => {
Object.values(room.players).forEach((p) => {
p.hand = [room.pioche.pop(), room.pioche.pop()];
p.bet = 0; p.folded = false; p.allin = false; p.last_played_turn = null;
p.bet = 0;
p.folded = false;
p.allin = false;
p.last_played_turn = null;
});
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
@@ -369,12 +433,14 @@ async function startNewHand(room, io) {
room.sb = sbPlayer.id;
room.bb = bbPlayer.id;
sbPlayer.bank -= 10; sbPlayer.bet = 10;
bbPlayer.bank -= 20; bbPlayer.bet = 20;
sbPlayer.bank -= 10;
sbPlayer.bet = 10;
bbPlayer.bank -= 20;
bbPlayer.bet = 20;
bbPlayer.last_played_turn = 0;
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
await emitPokerUpdate({ room: room, type: 'room-started' });
await emitPokerUpdate({ room: room, type: "room-started" });
}
async function checkRoundCompletion(room, io) {
@@ -389,43 +455,45 @@ async function checkRoundCompletion(room, io) {
}
} else {
room.current_player = getNextActivePlayer(room);
await emitPokerUpdate({ room: room, type: 'round-continue' });
await emitPokerUpdate({ room: room, type: "round-continue" });
}
}
async function advanceToNextPhase(room, io, phase) {
Object.values(room.players).forEach(p => { if (!p.folded) p.last_played_turn = null; });
Object.values(room.players).forEach((p) => {
if (!p.folded) p.last_played_turn = null;
});
switch (phase) {
case 'flop':
case "flop":
room.current_turn = 1;
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
break;
case 'turn':
case "turn":
room.current_turn = 2;
room.tapis.push(room.pioche.pop());
break;
case 'river':
case "river":
room.current_turn = 3;
room.tapis.push(room.pioche.pop());
break;
case 'showdown':
case "showdown":
await handleShowdown(room, io, checkRoomWinners(room));
return;
case 'progressive-showdown':
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
case "progressive-showdown":
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
while (room.tapis.length < 5) {
await sleep(500);
room.tapis.push(room.pioche.pop());
updatePlayerHandSolves(room);
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
}
await handleShowdown(room, io, checkRoomWinners(room));
return;
}
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
room.current_player = getFirstActivePlayerAfterDealer(room);
await emitPokerUpdate({ room: room, type: 'phase-advanced' });
await emitPokerUpdate({ room: room, type: "phase-advanced" });
}
async function handleShowdown(room, io, winners) {
@@ -436,11 +504,13 @@ async function handleShowdown(room, io, winners) {
room.current_player = null;
let totalPot = 0;
Object.values(room.players).forEach(p => { totalPot += p.bet; });
Object.values(room.players).forEach((p) => {
totalPot += p.bet;
});
const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0;
winners.forEach(winnerId => {
winners.forEach((winnerId) => {
const winnerPlayer = room.players[winnerId];
if (winnerPlayer) {
winnerPlayer.bank += winAmount;
@@ -450,13 +520,13 @@ async function handleShowdown(room, io, winners) {
await clearAfkPlayers(room);
//await pokerEloHandler(room);
await emitPokerUpdate({ room: room, type: 'showdown' });
await emitPokerUpdate({ room: room, type: "showdown" });
await emitPokerToast({
type: 'player-winner',
type: "player-winner",
playerIds: winners,
roomId: room.id,
amount: winAmount,
})
});
}
// NEW: Function to calculate and update hand strength for all players
@@ -479,14 +549,16 @@ function updatePlayerCoins(player, amount, isFake) {
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
insertLog.run({
id: `${player.id}-poker-${Date.now()}`,
user_id: player.id, target_user_id: null,
action: `POKER_${amount > 0 ? 'WIN' : 'LOSE'}`,
coins_amount: amount, user_new_amount: userDB.coins + amount,
user_id: player.id,
target_user_id: null,
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
coins_amount: amount,
user_new_amount: userDB.coins + amount,
});
}
async function clearAfkPlayers(room) {
Object.keys(room.afk).forEach(playerId => {
Object.keys(room.afk).forEach((playerId) => {
if (room.players[playerId]) {
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
delete room.players[playerId];

View File

@@ -1,17 +1,34 @@
import express from 'express';
import express from "express";
// --- Game Logic Imports ---
import {
createDeck, shuffle, deal, isValidMove, moveCard, drawCard,
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves
} from '../../game/solitaire.js';
createDeck,
shuffle,
deal,
isValidMove,
moveCard,
drawCard,
checkWinCondition,
createSeededRNG,
seededShuffle,
undoMove,
draw3Cards,
checkAutoSolve,
autoSolveMoves,
} from "../../game/solitaire.js";
// --- Game State & Database Imports ---
import { activeSolitaireGames } from '../../game/state.js';
import { activeSolitaireGames } from "../../game/state.js";
import {
getSOTD, getUser, insertSOTDStats, deleteUserSOTDStats,
getUserSOTDStats, updateUserCoins, insertLog, getAllSOTDStats
} from '../../database/index.js';
getSOTD,
getUser,
insertSOTDStats,
deleteUserSOTDStats,
getUserSOTDStats,
updateUserCoins,
insertLog,
getAllSOTDStats,
} from "../../database/index.js";
import { socketEmit } from "../socket.js";
// Create a new router instance
@@ -24,16 +41,18 @@ const router = express.Router();
* @returns {object} The configured Express router.
*/
export function solitaireRoutes(client, io) {
// --- Game Initialization Endpoints ---
router.post('/start', (req, res) => {
router.post("/start", (req, res) => {
const { userId, userSeed, hardMode } = req.body;
if (!userId) return res.status(400).json({ error: 'User ID is required.' });
if (!userId) return res.status(400).json({ error: "User ID is required." });
// If a game already exists for the user, return it instead of creating a new one.
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
return res.json({
success: true,
gameState: activeSolitaireGames[userId],
});
}
let deck, seed;
@@ -47,7 +66,7 @@ export function solitaireRoutes(client, io) {
let numericSeed = 0;
for (let i = 0; i < seed.length; i++) {
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xFFFFFFFF;
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xffffffff;
}
const rng = createSeededRNG(numericSeed);
@@ -66,19 +85,22 @@ export function solitaireRoutes(client, io) {
res.json({ success: true, gameState });
});
router.post('/start/sotd', (req, res) => {
router.post("/start/sotd", (req, res) => {
const { userId } = req.body;
/*if (!userId || !getUser.get(userId)) {
return res.status(404).json({ error: 'User not found.' });
}*/
if (activeSolitaireGames[userId]?.isSOTD) {
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
return res.json({
success: true,
gameState: activeSolitaireGames[userId],
});
}
const sotd = getSOTD.get();
if (!sotd) {
return res.status(500).json({ error: 'Solitaire of the Day is not configured.'});
return res.status(500).json({ error: "Solitaire of the Day is not configured." });
}
const gameState = {
@@ -104,7 +126,7 @@ export function solitaireRoutes(client, io) {
// --- Game State & Action Endpoints ---
router.get('/sotd/rankings', (req, res) => {
router.get("/sotd/rankings", (req, res) => {
try {
const rankings = getAllSOTDStats.all();
res.json({ rankings });
@@ -113,17 +135,17 @@ export function solitaireRoutes(client, io) {
}
});
router.get('/state/:userId', (req, res) => {
router.get("/state/:userId", (req, res) => {
const { userId } = req.params;
const gameState = activeSolitaireGames[userId];
if (gameState) {
res.json({ success: true, gameState });
} else {
res.status(404).json({ error: 'No active game found for this user.' });
res.status(404).json({ error: "No active game found for this user." });
}
});
router.post('/reset', (req, res) => {
router.post("/reset", (req, res) => {
const { userId } = req.body;
if (activeSolitaireGames[userId]) {
delete activeSolitaireGames[userId];
@@ -131,22 +153,22 @@ export function solitaireRoutes(client, io) {
res.json({ success: true, message: "Game reset." });
});
router.post('/move', async (req, res) => {
router.post("/move", async (req, res) => {
const { userId, ...moveData } = req.body;
const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
if (!gameState) return res.status(404).json({ error: "Game not found." });
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
if (isValidMove(gameState, moveData)) {
moveCard(gameState, moveData);
updateGameStats(gameState, 'move', moveData);
updateGameStats(gameState, "move", moveData);
if (!gameState.autocompleting) {
const canAutoSolve = checkAutoSolve(gameState);
if (canAutoSolve) {
gameState.autocompleting = true;
autoSolveMoves(userId, gameState)
autoSolveMoves(userId, gameState);
}
}
@@ -157,42 +179,41 @@ export function solitaireRoutes(client, io) {
}
res.json({ success: true, gameState, win });
} else {
res.status(400).json({ error: 'Invalid move' });
res.status(400).json({ error: "Invalid move" });
}
});
router.post('/draw', (req, res) => {
router.post("/draw", (req, res) => {
const { userId } = req.body;
const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
if (!gameState) return res.status(404).json({ error: "Game not found." });
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
if (gameState.hardMode) {
draw3Cards(gameState);
} else {
drawCard(gameState);
}
updateGameStats(gameState, 'draw');
updateGameStats(gameState, "draw");
res.json({ success: true, gameState });
});
router.post('/undo', (req, res) => {
router.post("/undo", (req, res) => {
const { userId } = req.body;
const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
if (gameState.hist.length === 0) return res.status(400).json({ error: 'No moves to undo.'});
if (!gameState) return res.status(404).json({ error: "Game not found." });
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
if (gameState.hist.length === 0) return res.status(400).json({ error: "No moves to undo." });
undoMove(gameState);
res.json({ success: true, gameState });
})
});
return router;
}
// --- Helper Functions ---
/** Updates game stats like moves and score after an action. */
@@ -200,15 +221,15 @@ function updateGameStats(gameState, actionType, moveData = {}) {
// if (!gameState.isSOTD) return; // Only track stats for SOTD
gameState.moves++;
if (actionType === 'move') {
if (moveData.destPileType === 'foundationPiles') {
if (actionType === "move") {
if (moveData.destPileType === "foundationPiles") {
gameState.score += 10; // Move card to foundation
}
if (moveData.sourcePileType === 'foundationPiles') {
if (moveData.sourcePileType === "foundationPiles") {
gameState.score -= 15; // Move card from foundation (penalty)
}
}
if(actionType === 'draw' && gameState.wastePile.length === 0) {
if (actionType === "draw" && gameState.wastePile.length === 0) {
// Penalty for cycling through an empty stock pile
gameState.score -= 5;
}
@@ -224,11 +245,14 @@ async function handleWin(userId, gameState, io) {
const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({
id: `${userId}-hardmode-solitaire-${Date.now()}`, user_id: userId,
action: 'HARDMODE_SOLITAIRE_WIN', target_user_id: null,
coins_amount: bonus, user_new_amount: newCoins,
id: `${userId}-hardmode-solitaire-${Date.now()}`,
user_id: userId,
action: "HARDMODE_SOLITAIRE_WIN",
target_user_id: null,
coins_amount: bonus,
user_new_amount: newCoins,
});
await socketEmit('data-updated', { table: 'users' });
await socketEmit("data-updated", { table: "users" });
}
if (!gameState.isSOTD) return; // Only process SOTD wins here
@@ -244,28 +268,35 @@ async function handleWin(userId, gameState, io) {
const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({
id: `${userId}-sotd-complete-${Date.now()}`, user_id: userId,
action: 'SOTD_WIN', target_user_id: null,
coins_amount: bonus, user_new_amount: newCoins,
id: `${userId}-sotd-complete-${Date.now()}`,
user_id: userId,
action: "SOTD_WIN",
target_user_id: null,
coins_amount: bonus,
user_new_amount: newCoins,
});
await socketEmit('data-updated', { table: 'users' });
await socketEmit("data-updated", { table: "users" });
}
// Save the score if it's better than the previous one
const isNewBest = !existingStats ||
const isNewBest =
!existingStats ||
gameState.score > existingStats.score ||
(gameState.score === existingStats.score && gameState.moves < existingStats.moves) ||
(gameState.score === existingStats.score && gameState.moves === existingStats.moves && timeTaken < existingStats.time);
(gameState.score === existingStats.score &&
gameState.moves === existingStats.moves &&
timeTaken < existingStats.time);
if (isNewBest) {
deleteUserSOTDStats.run(userId)
deleteUserSOTDStats.run(userId);
insertSOTDStats.run({
id: userId, user_id: userId,
id: userId,
user_id: userId,
time: timeTaken,
moves: gameState.moves,
score: gameState.score,
});
await socketEmit('sotd-update')
await socketEmit("sotd-update");
console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`);
}
}

View File

@@ -1,13 +1,20 @@
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import {
activeTicTacToeGames,
tictactoeQueue,
activeConnect4Games,
connect4Queue,
queueMessagesEndpoints, activePredis
} from '../game/state.js';
import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js';
import { eloHandler } from '../game/elo.js';
queueMessagesEndpoints,
activePredis,
} from "../game/state.js";
import {
createConnect4Board,
formatConnect4BoardForDiscord,
checkConnect4Win,
checkConnect4Draw,
C4_ROWS,
} from "../game/various.js";
import { eloHandler } from "../game/elo.js";
import { getUser } from "../database/index.js";
// --- Module-level State ---
@@ -18,8 +25,8 @@ let io;
export function initializeSocket(server, client) {
io = server;
io.on('connection', (socket) => {
socket.on('user-connected', async (userId) => {
io.on("connection", (socket) => {
socket.on("user-connected", async (userId) => {
if (!userId) return;
await refreshQueuesForUser(userId, client);
});
@@ -27,15 +34,15 @@ export function initializeSocket(server, client) {
registerTicTacToeEvents(socket, client);
registerConnect4Events(socket, client);
socket.on('tictactoe:queue:leave', async ({ discordId }) => await refreshQueuesForUser(discordId, client));
socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
// catch tab kills / network drops
socket.on('disconnecting', async () => {
socket.on("disconnecting", async () => {
const discordId = socket.handshake.auth?.discordId; // or your mapping
await refreshQueuesForUser(discordId, client);
});
socket.on('disconnect', () => {
socket.on("disconnect", () => {
//
});
});
@@ -50,17 +57,17 @@ export function getSocketIo() {
// --- Event Registration ---
function registerTicTacToeEvents(socket, client) {
socket.on('tictactoeconnection', (e) => refreshQueuesForUser(e.id, client));
socket.on('tictactoequeue', (e) => onQueueJoin(client, 'tictactoe', e.playerId));
socket.on('tictactoeplaying', (e) => onTicTacToeMove(client, e));
socket.on('tictactoegameOver', (e) => onGameOver(client, 'tictactoe', e.playerId, e.winner));
socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client));
socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId));
socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e));
socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner));
}
function registerConnect4Events(socket, client) {
socket.on('connect4connection', (e) => refreshQueuesForUser(e.id, client));
socket.on('connect4queue', (e) => onQueueJoin(client, 'connect4', e.playerId));
socket.on('connect4playing', (e) => onConnect4Move(client, e));
socket.on('connect4NoTime', (e) => onGameOver(client, 'connect4', e.playerId, e.winner, '(temps écoulé)'));
socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client));
socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId));
socket.on("connect4playing", (e) => onConnect4Move(client, e));
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
}
// --- Core Handlers (Preserving Original Logic) ---
@@ -69,7 +76,10 @@ async function onQueueJoin(client, gameType, playerId) {
if (!playerId) return;
const { queue, activeGames, title, url } = getGameAssets(gameType);
if (queue.includes(playerId) || Object.values(activeGames).some(g => g.p1.id === playerId || g.p2.id === playerId)) {
if (
queue.includes(playerId) ||
Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)
) {
return;
}
@@ -89,12 +99,17 @@ async function onQueueJoin(client, gameType, playerId) {
*/
function checkTicTacToeWin(moves) {
const winningCombinations = [
[1, 2, 3], [4, 5, 6], [7, 8, 9], // Rows
[1, 4, 7], [2, 5, 8], [3, 6, 9], // Columns
[1, 5, 9], [3, 5, 7] // Diagonals
[1, 2, 3],
[4, 5, 6],
[7, 8, 9], // Rows
[1, 4, 7],
[2, 5, 8],
[3, 6, 9], // Columns
[1, 5, 9],
[3, 5, 7], // Diagonals
];
for (const combination of winningCombinations) {
if (combination.every(num => moves.includes(num))) {
if (combination.every((num) => moves.includes(num))) {
return true;
}
}
@@ -103,11 +118,13 @@ function checkTicTacToeWin(moves) {
async function onTicTacToeMove(client, eventData) {
const { playerId, value, boxId } = eventData;
const lobby = Object.values(activeTicTacToeGames).find(g => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver);
const lobby = Object.values(activeTicTacToeGames).find(
(g) => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver,
);
if (!lobby) return;
const isP1Turn = lobby.sum % 2 === 1 && value === 'X' && lobby.p1.id === playerId;
const isP2Turn = lobby.sum % 2 === 0 && value === 'O' && lobby.p2.id === playerId;
const isP1Turn = lobby.sum % 2 === 1 && value === "X" && lobby.p1.id === playerId;
const isP2Turn = lobby.sum % 2 === 0 && value === "O" && lobby.p2.id === playerId;
if (isP1Turn || isP2Turn) {
const playerMoves = isP1Turn ? lobby.xs : lobby.os;
@@ -115,28 +132,32 @@ async function onTicTacToeMove(client, eventData) {
lobby.sum++;
lobby.lastmove = Date.now();
if (isP1Turn) lobby.p1.move = boxId
if (isP2Turn) lobby.p2.move = boxId
if (isP1Turn) lobby.p1.move = boxId;
if (isP2Turn) lobby.p2.move = boxId;
io.emit('tictactoeplaying', { allPlayers: Object.values(activeTicTacToeGames) });
io.emit("tictactoeplaying", {
allPlayers: Object.values(activeTicTacToeGames),
});
const hasWon = checkTicTacToeWin(playerMoves);
if (hasWon) {
// The current player has won. End the game.
await onGameOver(client, 'tictactoe', playerId, playerId);
await onGameOver(client, "tictactoe", playerId, playerId);
} else if (lobby.sum > 9) {
// It's a draw (9 moves made, sum is now 10). End the game.
await onGameOver(client, 'tictactoe', playerId, null); // null winner for a draw
await onGameOver(client, "tictactoe", playerId, null); // null winner for a draw
} else {
// The game continues. Update the state and notify clients.
await updateDiscordMessage(client, lobby, 'Tic Tac Toe');
await updateDiscordMessage(client, lobby, "Tic Tac Toe");
}
}
await emitQueueUpdate(client, 'tictactoe');
await emitQueueUpdate(client, "tictactoe");
}
async function onConnect4Move(client, eventData) {
const { playerId, col } = eventData;
const lobby = Object.values(activeConnect4Games).find(l => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver);
const lobby = Object.values(activeConnect4Games).find(
(l) => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver,
);
if (!lobby || lobby.turn !== playerId) return;
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
@@ -160,17 +181,19 @@ async function onConnect4Move(client, eventData) {
winnerId = null; // Represents a draw
} else {
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
io.emit('connect4playing', { allPlayers: Object.values(activeConnect4Games) });
await emitQueueUpdate(client, 'connact4');
await updateDiscordMessage(client, lobby, 'Puissance 4');
io.emit("connect4playing", {
allPlayers: Object.values(activeConnect4Games),
});
await emitQueueUpdate(client, "connact4");
await updateDiscordMessage(client, lobby, "Puissance 4");
return;
}
await onGameOver(client, 'connect4', playerId, winnerId);
await onGameOver(client, "connect4", playerId, winnerId);
}
async function onGameOver(client, gameType, playerId, winnerId, reason = '') {
async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
const { activeGames, title } = getGameAssets(gameType);
const gameKey = Object.keys(activeGames).find(key => key.includes(playerId));
const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId));
const game = gameKey ? activeGames[gameKey] : undefined;
if (!game || game.gameOver) return;
@@ -178,20 +201,26 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = '') {
let resultText;
if (winnerId === null) {
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
resultText = 'Égalité';
resultText = "Égalité";
} else {
await eloHandler(game.p1.id, game.p2.id, game.p1.id === winnerId ? 1 : 0, game.p2.id === winnerId ? 1 : 0, title.toUpperCase());
await eloHandler(
game.p1.id,
game.p2.id,
game.p1.id === winnerId ? 1 : 0,
game.p2.id === winnerId ? 1 : 0,
title.toUpperCase(),
);
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
resultText = `Victoire de ${winnerName}`;
}
await updateDiscordMessage(client, game, title, `${resultText} ${reason}`);
if(gameType === 'tictactoe') io.emit('tictactoegameOver', { game, winner: winnerId });
if(gameType === 'connect4') io.emit('connect4gameOver', { game, winner: winnerId });
if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
if (gameKey) {
setTimeout(() => delete activeGames[gameKey], 1000)
setTimeout(() => delete activeGames[gameKey], 1000);
}
}
@@ -204,10 +233,47 @@ async function createGame(client, gameType) {
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
let lobby;
if (gameType === 'tictactoe') {
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'X', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'O', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, sum: 1, xs: [], os: [], gameOver: false, lastmove: Date.now() };
} else { // connect4
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'R', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'Y', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, turn: p1Id, board: createConnect4Board(), gameOver: false, lastmove: Date.now(), winningPieces: [] };
if (gameType === "tictactoe") {
lobby = {
p1: {
id: p1Id,
name: p1.globalName,
val: "X",
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
},
p2: {
id: p2Id,
name: p2.globalName,
val: "O",
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
},
sum: 1,
xs: [],
os: [],
gameOver: false,
lastmove: Date.now(),
};
} else {
// connect4
lobby = {
p1: {
id: p1Id,
name: p1.globalName,
val: "R",
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
},
p2: {
id: p2Id,
name: p2.globalName,
val: "Y",
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
},
turn: p1Id,
board: createConnect4Board(),
gameOver: false,
lastmove: Date.now(),
winningPieces: [],
};
}
const msgId = await updateDiscordMessage(client, lobby, title);
@@ -230,12 +296,16 @@ async function refreshQueuesForUser(userId, client) {
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
const updatedEmbed = new EmbedBuilder().setTitle('Tic Tac Toe').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
const updatedEmbed = new EmbedBuilder()
.setTitle("Tic Tac Toe")
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
.setColor(0xed4245)
.setTimestamp(new Date());
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId];
} catch (e) {
console.error('Error updating queue message : ', e);
console.error("Error updating queue message : ", e);
}
}
@@ -245,31 +315,52 @@ async function refreshQueuesForUser(userId, client) {
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
const updatedEmbed = new EmbedBuilder().setTitle('Puissance 4').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
const updatedEmbed = new EmbedBuilder()
.setTitle("Puissance 4")
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
.setColor(0xed4245)
.setTimestamp(new Date());
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId];
} catch (e) {
console.error('Error updating queue message : ', e);
console.error("Error updating queue message : ", e);
}
}
await emitQueueUpdate(client, 'tictactoe');
await emitQueueUpdate(client, 'connect4');
await emitQueueUpdate(client, "tictactoe");
await emitQueueUpdate(client, "connect4");
}
async function emitQueueUpdate(client, gameType) {
const { queue, activeGames } = getGameAssets(gameType);
const names = await Promise.all(queue.map(async (id) => {
const names = await Promise.all(
queue.map(async (id) => {
const user = await client.users.fetch(id).catch(() => null);
return user?.globalName || user?.username;
}));
io.emit(`${gameType}queue`, { allPlayers: Object.values(activeGames), queue: names.filter(Boolean) });
}),
);
io.emit(`${gameType}queue`, {
allPlayers: Object.values(activeGames),
queue: names.filter(Boolean),
});
}
function getGameAssets(gameType) {
if (gameType === 'tictactoe') return { queue: tictactoeQueue, activeGames: activeTicTacToeGames, title: 'Tic Tac Toe', url: '/tic-tac-toe' };
if (gameType === 'connect4') return { queue: connect4Queue, activeGames: activeConnect4Games, title: 'Puissance 4', url: '/connect-4' };
if (gameType === "tictactoe")
return {
queue: tictactoeQueue,
activeGames: activeTicTacToeGames,
title: "Tic Tac Toe",
url: "/tic-tac-toe",
};
if (gameType === "connect4")
return {
queue: connect4Queue,
activeGames: activeConnect4Games,
title: "Puissance 4",
url: "/connect-4",
};
return { queue: [], activeGames: {} };
}
@@ -277,28 +368,48 @@ async function postQueueToDiscord(client, playerId, title, url) {
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(playerId);
const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2').setTimestamp(new Date());
const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel(`Jouer contre ${user.username}`).setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`).setStyle(ButtonStyle.Link));
const msg = await generalChannel.send({ embeds: [embed], components: [row] });
queueMessagesEndpoints[playerId] = msg.id
} catch (e) { console.error(`Failed to post queue message for ${title}:`, e); }
const embed = new EmbedBuilder()
.setTitle(title)
.setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`)
.setColor("#5865F2")
.setTimestamp(new Date());
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel(`Jouer contre ${user.username}`)
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`)
.setStyle(ButtonStyle.Link),
);
const msg = await generalChannel.send({
embeds: [embed],
components: [row],
});
queueMessagesEndpoints[playerId] = msg.id;
} catch (e) {
console.error(`Failed to post queue message for ${title}:`, e);
}
}
async function updateDiscordMessage(client, game, title, resultText = '') {
async function updateDiscordMessage(client, game, title, resultText = "") {
const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
if (!channel) return null;
let description;
if (title === 'Tic Tac Toe') {
let gridText = '';
for (let i = 1; i <= 9; i++) { gridText += game.xs.includes(i) ? '❌' : game.os.includes(i) ? '⭕' : '🟦'; if (i % 3 === 0) gridText += '\n'; }
if (title === "Tic Tac Toe") {
let gridText = "";
for (let i = 1; i <= 9; i++) {
gridText += game.xs.includes(i) ? "❌" : game.os.includes(i) ? "⭕" : "🟦";
if (i % 3 === 0) gridText += "\n";
}
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
} else {
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
}
if (resultText) description += `\n### ${resultText}`;
const embed = new EmbedBuilder().setTitle(title).setDescription(description).setColor(game.gameOver ? '#2ade2a' : '#5865f2');
const embed = new EmbedBuilder()
.setTitle(title)
.setDescription(description)
.setColor(game.gameOver ? "#2ade2a" : "#5865f2");
try {
if (game.msgId) {
@@ -309,22 +420,24 @@ async function updateDiscordMessage(client, game, title, resultText = '') {
const message = await channel.send({ embeds: [embed] });
return message.id;
}
} catch (e) { return null; }
} catch (e) {
return null;
}
}
function cleanupStaleGames() {
const now = Date.now();
const STALE_TIMEOUT = 30 * 60 * 1000;
const cleanup = (games, name) => {
Object.keys(games).forEach(key => {
Object.keys(games).forEach((key) => {
if (now - games[key].lastmove > STALE_TIMEOUT) {
console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
delete games[key];
}
});
};
cleanup(activeTicTacToeGames, 'TicTacToe');
cleanup(activeConnect4Games, 'Connect4');
cleanup(activeTicTacToeGames, "TicTacToe");
cleanup(activeConnect4Games, "Connect4");
}
/* EMITS */
@@ -332,18 +445,18 @@ export async function socketEmit(event, data) {
io.emit(event, data);
}
export async function emitDataUpdated(data) {
io.emit('data-updated', data);
io.emit("data-updated", data);
}
export async function emitPokerUpdate(data) {
io.emit('poker-update', data);
io.emit("poker-update", data);
}
export async function emitPokerToast(data) {
io.emit('poker-toast', data);
io.emit("poker-toast", data);
}
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
export const emitSolitaireUpdate = (userId, moves) => io.emit('solitaire:update', {userId, moves});
export const emitSolitaireUpdate = (userId, moves) => io.emit("solitaire:update", { userId, moves });

View File

@@ -1,7 +1,7 @@
import 'dotenv/config';
import "dotenv/config";
import OpenAI from "openai";
import { GoogleGenAI } from "@google/genai";
import {Mistral} from '@mistralai/mistralai';
import { Mistral } from "@mistralai/mistralai";
// --- AI Client Initialization ---
// Initialize clients for each AI service. This is done once when the module is loaded.
@@ -13,7 +13,7 @@ if (process.env.OPENAI_API_KEY) {
let gemini;
if (process.env.GEMINI_KEY) {
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY})
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY });
}
let mistral;
@@ -21,7 +21,6 @@ if (process.env.MISTRAL_KEY) {
mistral = new Mistral({ apiKey: process.env.MISTRAL_KEY });
}
/**
* Gets a response from the configured AI model.
* It dynamically chooses the provider based on the MODEL environment variable.
@@ -35,7 +34,7 @@ export async function gork(messageHistory) {
try {
// --- OpenAI Provider ---
if (modelProvider === 'OpenAI' && openai) {
if (modelProvider === "OpenAI" && openai) {
const completion = await openai.chat.completions.create({
model: "gpt-5", // Using a modern, cost-effective model
reasoning_effort: "low",
@@ -45,15 +44,15 @@ export async function gork(messageHistory) {
}
// --- Google Gemini Provider ---
else if (modelProvider === 'Gemini' && gemini) {
else if (modelProvider === "Gemini" && gemini) {
// Gemini requires a slightly different history format.
const contents = messageHistory.map(msg => ({
role: msg.role === 'assistant' ? 'model' : msg.role, // Gemini uses 'model' for assistant role
const contents = messageHistory.map((msg) => ({
role: msg.role === "assistant" ? "model" : msg.role, // Gemini uses 'model' for assistant role
parts: [{ text: msg.content }],
}));
// The last message should not be from the model
if (contents[contents.length - 1].role === 'model') {
if (contents[contents.length - 1].role === "model") {
contents.pop();
}
@@ -63,9 +62,9 @@ export async function gork(messageHistory) {
}
// --- Mistral Provider ---
else if (modelProvider === 'Mistral' && mistral) {
else if (modelProvider === "Mistral" && mistral) {
const chatResponse = await mistral.chat({
model: 'mistral-large-latest',
model: "mistral-large-latest",
messages: messageHistory,
});
return chatResponse.choices[0].message.content;
@@ -82,17 +81,16 @@ export async function gork(messageHistory) {
}
}
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || '100', 10);
export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || '3', 10);
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || 'true') === 'true';
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || "100", 10);
export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || "3", 10);
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || "true") === "true";
export const stripMentionsOfBot = (text, botId) =>
text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim();
export const stripMentionsOfBot = (text, botId) => text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
export const sanitize = (s) =>
(s || '')
.replace(/\s+/g, ' ')
.replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels
(s || "")
.replace(/\s+/g, " ")
.replace(/```/g, "ʼʼʼ") // éviter de casser des fences éventuels
.trim();
export const shortTs = (d) => new Date(d).toISOString(); // compact et triable
@@ -122,15 +120,15 @@ export function buildTranscript(messages, botId) {
if (!content && atts.length === 0) continue;
const attMeta = atts.length
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map((a) => ({
id: a.id,
name: a.name,
type: a.contentType || 'application/octet-stream',
type: a.contentType || "application/octet-stream",
size: a.size,
isImage: !!(a.contentType && a.contentType.startsWith('image/')),
isImage: !!(a.contentType && a.contentType.startsWith("image/")),
width: a.width || undefined,
height: a.height || undefined,
spoiler: typeof a.spoiler === 'boolean' ? a.spoiler : false,
spoiler: typeof a.spoiler === "boolean" ? a.spoiler : false,
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
}))
: undefined;
@@ -140,19 +138,19 @@ export function buildTranscript(messages, botId) {
id: m.author.id,
nick: m.member?.nickname || m.author.globalName || m.author.username,
isBot: !!m.author.bot,
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''),
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ""),
replyTo: m.reference?.messageId || null,
content,
attachments: attMeta,
};
lines.push(line);
}
return lines.map(l => JSON.stringify(l)).join('\n');
return lines.map((l) => JSON.stringify(l)).join("\n");
}
export function buildAiMessages({
botId,
botName = 'FlopoBot',
botName = "FlopoBot",
invokerId,
invokerName,
requestText,
@@ -162,9 +160,8 @@ export function buildAiMessages({
invokerAttachments = [],
}) {
const system = {
role: 'system',
content:
`Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
role: "system",
content: `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
Règles de sortie:
- Réponds en français, en 13 phrases.
- Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif.
@@ -174,24 +171,31 @@ export function buildAiMessages({
};
const attLines = invokerAttachments.length
? invokerAttachments.map(a => `- ${a.name} (${a.type || 'type inconnu'}, ${a.size ?? '?'} o${a.isImage ? ', image' : ''})`).join('\n')
: '';
? invokerAttachments
.map((a) => `- ${a.name} (${a.type || "type inconnu"}, ${a.size ?? "?"} o${a.isImage ? ", image" : ""})`)
.join("\n")
: "";
const user = {
role: 'user',
content:
`Tâche: répondre brièvement à <@${invokerId}>.
role: "user",
content: `Tâche: répondre brièvement à <@${invokerId}>.
Message de <@${invokerId}> (${invokerName || 'inconnu'}):
Message de <@${invokerId}> (${invokerName || "inconnu"}):
"""
${requestText}
"""
${invokerAttachments.length ? `Pièces jointes du message:
${
invokerAttachments.length
? `Pièces jointes du message:
${attLines}
` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''}
`
: ""
}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ""}
Participants (id -> nom):
${Object.values(participants).map(p => `- ${p.id} -> ${p.globalName || p.username}`).join('\n')}
${Object.values(participants)
.map((p) => `- ${p.id} -> ${p.globalName || p.username}`)
.join("\n")}
Contexte (transcript JSONL; à utiliser seulement si utile):
\`\`\`jsonl

View File

@@ -1,75 +1,75 @@
export const roles = {
erynie_1: {
name: 'Erinye',
subtitle: 'Mégère, la haine',
descr: '',
name: "Erinye",
subtitle: "Mégère, la haine",
descr: "",
powers: {
double_vote: {
descr: 'Les Erinyes peuvent tuer une deuxième personne (1 seule fois).',
descr: "Les Erinyes peuvent tuer une deuxième personne (1 seule fois).",
charges: 1,
disabled: false,
},
},
passive: {},
team: 'Erinyes',
team: "Erinyes",
},
erynie_2: {
name: 'Erinye',
subtitle: 'Tisiphone, la vengeance',
descr: '',
name: "Erinye",
subtitle: "Tisiphone, la vengeance",
descr: "",
powers: {
one_shot: {
descr: 'Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).',
descr: "Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).",
charges: 1,
disabled: false,
},
},
passive: {},
team: 'Erinyes',
team: "Erinyes",
},
erynie_3: {
name: 'Erinye',
subtitle: 'Alecto, l\'implacable',
descr: '',
name: "Erinye",
subtitle: "Alecto, l'implacable",
descr: "",
powers: {
silence: {
descr: 'Empêche l\'utilisation du pouvoir de quelqu\'un pour le prochain tour.',
descr: "Empêche l'utilisation du pouvoir de quelqu'un pour le prochain tour.",
charges: 999,
disabled: false,
}
},
},
passive: {
descr: 'Voit quels pouvoirs ont été utilisés.',
descr: "Voit quels pouvoirs ont été utilisés.",
disabled: false,
},
team: 'Erinyes',
team: "Erinyes",
},
narcisse: {
name: 'Narcisse',
subtitle: '',
descr: '',
name: "Narcisse",
subtitle: "",
descr: "",
powers: {},
passive: {
descr: 'S\'il devient maire ...',
descr: "S'il devient maire ...",
disabled: false,
},
},
charon: {
name: 'Charon',
subtitle: 'Sorcier',
descr: 'C\'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.',
name: "Charon",
subtitle: "Sorcier",
descr: "C'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.",
powers: {
revive: {
descr: 'Refuser de faire traverser le Styx (sauver quelqu\'un)',
descr: "Refuser de faire traverser le Styx (sauver quelqu'un)",
charges: 1,
disabled: false,
},
kill: {
descr: 'Traverser le Styx (tuer quelqu\'un)',
descr: "Traverser le Styx (tuer quelqu'un)",
charges: 1,
disabled: false,
}
},
},
},
//...
}
};

View File

@@ -1,16 +1,19 @@
import 'dotenv/config';
import cron from 'node-cron';
import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
import "dotenv/config";
import cron from "node-cron";
// --- Local Imports ---
import { getValorantSkins, getSkinTiers } from '../api/valorant.js';
import { DiscordRequest } from '../api/discord.js';
import { initTodaysSOTD } from '../game/points.js';
import { getSkinTiers, getValorantSkins } from "../api/valorant.js";
import { DiscordRequest } from "../api/discord.js";
import { initTodaysSOTD } from "../game/points.js";
import {
insertManyUsers, insertManySkins, resetDailyReward,
pruneOldLogs, getAllUsers as dbGetAllUsers, getSOTD, getUser, getAllUsers, insertUser, stmtUsers,
} from '../database/index.js';
import { activeInventories, activeSearchs, activePredis, pokerRooms, skins } from '../game/state.js';
getAllAkhys,
getAllUsers,
insertManySkins,
insertUser,
resetDailyReward,
updateUserAvatar,
} from "../database/index.js";
import { activeInventories, activeSearchs, skins } from "../game/state.js";
export async function InstallGlobalCommands(appId, commands) {
// API endpoint to overwrite global commands
@@ -18,7 +21,7 @@ export async function InstallGlobalCommands(appId, commands) {
try {
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
await DiscordRequest(endpoint, { method: "PUT", body: commands });
} catch (err) {
console.error(err);
}
@@ -37,10 +40,9 @@ export async function getAkhys(client) {
const initial_akhys = getAllUsers.all().length;
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const members = await guild.members.fetch();
const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
const usersToInsert = akhys.map(akhy => ({
const usersToInsert = akhys.map((akhy) => ({
id: akhy.user.id,
username: akhy.user.username,
globalName: akhy.user.globalName,
@@ -49,31 +51,34 @@ export async function getAkhys(client) {
allTimeWarns: 0,
totalRequests: 0,
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
isAkhy: 1
isAkhy: 1,
}));
if (usersToInsert.length > 0) {
usersToInsert.forEach(user => {
try { insertUser.run(user) } catch (err) {}
})
usersToInsert.forEach((user) => {
try {
insertUser.run(user);
} catch (err) {}
});
}
const new_akhys = getAllUsers.all().length;
const diff = new_akhys - initial_akhys
console.log(`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? '(' + (diff > 0 ? '+' + diff : diff) + ') ' : ''}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`);
const diff = new_akhys - initial_akhys;
console.log(
`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`,
);
// 2. Fetch Valorant Skins
const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]);
// Clear and rebuild the in-memory skin cache
skins.length = 0;
fetchedSkins.forEach(skin => skins.push(skin));
fetchedSkins.forEach((skin) => skins.push(skin));
const skinsToInsert = fetchedSkins
.filter(skin => skin.contentTierUuid)
.map(skin => {
const tier = fetchedTiers.find(t => t.uuid === skin.contentTierUuid) || {};
.filter((skin) => skin.contentTierUuid)
.map((skin) => {
const tier = fetchedTiers.find((t) => t.uuid === skin.contentTierUuid) || {};
const basePrice = calculateBasePrice(skin, tier.rank);
return {
uuid: skin.uuid,
@@ -82,7 +87,7 @@ export async function getAkhys(client) {
displayIcon: skin.displayIcon,
user_id: null,
tierRank: tier.rank,
tierColor: tier.highlightColor?.slice(0, 6) || 'F2F3F3',
tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3",
tierText: formatTierText(tier.rank, skin.displayName),
basePrice: basePrice.toFixed(0),
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0),
@@ -93,13 +98,11 @@ export async function getAkhys(client) {
insertManySkins(skinsToInsert);
}
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
} catch (err) {
console.error('Error during initial data sync (getAkhys):', err);
console.error("Error during initial data sync (getAkhys):", err);
}
}
// --- Cron Jobs / Scheduled Tasks ---
/**
@@ -109,7 +112,7 @@ export async function getAkhys(client) {
*/
export function setupCronJobs(client, io) {
// Every 10 minutes: Clean up expired interactive sessions
cron.schedule('*/10 * * * *', () => {
cron.schedule("*/10 * * * *", () => {
const now = Date.now();
const FIVE_MINUTES = 5 * 60 * 1000;
const ONE_DAY = 24 * 60 * 60 * 1000;
@@ -125,55 +128,70 @@ export function setupCronJobs(client, io) {
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
};
cleanup(activeInventories, 'inventory');
cleanup(activeSearchs, 'search');
cleanup(activeInventories, "inventory");
cleanup(activeSearchs, "search");
// Cleanup for predis and poker rooms...
// TODO: Cleanup for predis and poker rooms...
// ...
});
// Daily at midnight: Reset daily rewards and init SOTD
cron.schedule('0 0 * * *', async () => {
console.log('[Cron] Running daily midnight tasks...');
cron.schedule("0 0 * * *", async () => {
console.log("[Cron] Running daily midnight tasks...");
try {
resetDailyReward.run();
console.log('[Cron] Daily rewards have been reset for all users.');
console.log("[Cron] Daily rewards have been reset for all users.");
//if (!getSOTD.get()) {
initTodaysSOTD();
//}
} catch (e) {
console.error('[Cron] Error during daily reset:', e);
console.error("[Cron] Error during daily reset:", e);
}
});
// Daily at 7 AM: Re-sync users and skins
cron.schedule('0 7 * * *', async () => {
console.log('[Cron] Running daily 7 AM data sync...');
cron.schedule("0 7 * * *", async () => {
console.log("[Cron] Running daily 7 AM data sync...");
await getAkhys(client);
try {
const akhys = getAllAkhys.all();
for (const akhy of akhys) {
const user = await client.users.cache.get(akhy.id);
try {
updateUserAvatar.run({
id: akhy.id,
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
});
} catch (err) {
console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err);
}
}
} catch (e) {
console.error("[Cron] Error during daily avatar update:", e);
}
});
}
// --- Formatting Helpers ---
export function capitalize(str) {
if (typeof str !== 'string' || str.length === 0) return '';
if (typeof str !== "string" || str.length === 0) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function formatTime(seconds) {
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor(seconds % (3600*24) / 3600);
const m = Math.floor(seconds % 3600 / 60);
const h = Math.floor((seconds % (3600 * 24)) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const parts = [];
if (d > 0) parts.push(`**${d}** jour${d > 1 ? 's' : ''}`);
if (h > 0) parts.push(`**${h}** heure${h > 1 ? 's' : ''}`);
if (m > 0) parts.push(`**${m}** minute${m > 1 ? 's' : ''}`);
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? 's' : ''}`);
if (d > 0) parts.push(`**${d}** jour${d > 1 ? "s" : ""}`);
if (h > 0) parts.push(`**${h}** heure${h > 1 ? "s" : ""}`);
if (m > 0) parts.push(`**${m}** minute${m > 1 ? "s" : ""}`);
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? "s" : ""}`);
return parts.join(', ').replace(/,([^,]*)$/, ' et$1');
return parts.join(", ").replace(/,([^,]*)$/, " et$1");
}
// --- External API Helpers ---
@@ -188,7 +206,7 @@ export async function getAPOUsers() {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} catch (error) {
console.error('Error fetching APO users:', error);
console.error("Error fetching APO users:", error);
return null;
}
}
@@ -200,111 +218,123 @@ export async function getAPOUsers() {
*/
export async function postAPOBuy(userId, amount) {
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
return fetch(fetchUrl, { method: 'POST' });
return fetch(fetchUrl, { method: "POST" });
}
// --- Miscellaneous Helpers ---
export async function getOnlineUsersWithRole(guild, roleId) {
if (!guild || !roleId) return new Map();
try {
const members = await guild.members.fetch();
return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.presence?.status !== undefined && m.roles.cache.has(roleId));
return members.filter(
(m) =>
!m.user.bot &&
m.presence?.status !== "offline" &&
m.presence?.status !== undefined &&
m.roles.cache.has(roleId),
);
} catch (err) {
console.error('Error fetching online members with role:', err);
console.error("Error fetching online members with role:", err);
return new Map();
}
}
export function getRandomEmoji(list = 0) {
const emojiLists = [
['😭','😄','😌','🤓','😎','😤','🤖','😶‍🌫️','🌏','📸','💿','👋','🌊','✨'],
['<:CAUGHT:1323810730155446322>', '<:hinhinhin:1072510144933531758>', '<:o7:1290773422451986533>', '<:zhok:1115221772623683686>', '<:nice:1154049521110765759>', '<:nerd:1087658195603951666>', '<:peepSelfie:1072508131839594597>'],
["😭", "😄", "😌", "🤓", "😎", "😤", "🤖", "😶‍🌫️", "🌏", "📸", "💿", "👋", "🌊", "✨"],
[
"<:CAUGHT:1323810730155446322>",
"<:hinhinhin:1072510144933531758>",
"<:o7:1290773422451986533>",
"<:zhok:1115221772623683686>",
"<:nice:1154049521110765759>",
"<:nerd:1087658195603951666>",
"<:peepSelfie:1072508131839594597>",
],
];
const selectedList = emojiLists[list] || [''];
const selectedList = emojiLists[list] || [""];
return selectedList[Math.floor(Math.random() * selectedList.length)];
}
export function formatAmount(amount) {
if (amount >= 1000000000) {
amount /= 1000000000
amount /= 1000000000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'Md'
)
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "Md"
);
}
if (amount >= 1000000) {
amount /= 1000000
amount /= 1000000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'M'
)
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "M"
);
}
if (amount >= 10000) {
amount /= 1000
amount /= 1000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'K'
)
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "K"
);
}
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
// --- Private Helpers ---
export function calculateBasePrice(skin, tierRank) {
const name = skin.displayName.toLowerCase();
let price = 6000; // Default for melee
if (name.includes('classic')) price = 150;
else if (name.includes('shorty')) price = 300;
else if (name.includes('frenzy')) price = 450;
else if (name.includes('ghost')) price = 500;
else if (name.includes('sheriff')) price = 800;
else if (name.includes('stinger')) price = 1000;
else if (name.includes('spectre')) price = 1600;
else if (name.includes('bucky')) price = 900;
else if (name.includes('judge')) price = 1500;
else if (name.includes('bulldog')) price = 2100;
else if (name.includes('guardian')) price = 2700
else if (name.includes('vandal') || name.includes('phantom')) price = 2900;
else if (name.includes('marshal')) price = 950;
else if (name.includes('outlaw')) price = 2400;
else if (name.includes('operator')) price = 4500;
else if (name.includes('ares')) price = 1700;
else if (name.includes('odin')) price = 3200;
if (name.includes("classic")) price = 150;
else if (name.includes("shorty")) price = 300;
else if (name.includes("frenzy")) price = 450;
else if (name.includes("ghost")) price = 500;
else if (name.includes("sheriff")) price = 800;
else if (name.includes("stinger")) price = 1000;
else if (name.includes("spectre")) price = 1600;
else if (name.includes("bucky")) price = 900;
else if (name.includes("judge")) price = 1500;
else if (name.includes("bulldog")) price = 2100;
else if (name.includes("guardian")) price = 2700;
else if (name.includes("vandal") || name.includes("phantom")) price = 2900;
else if (name.includes("marshal")) price = 950;
else if (name.includes("outlaw")) price = 2400;
else if (name.includes("operator")) price = 4500;
else if (name.includes("ares")) price = 1700;
else if (name.includes("odin")) price = 3200;
price *= (1 + (tierRank || 0));
if (name.includes('vct')) price *= 1.25;
if (name.includes('champions')) price *= 2;
price *= 1 + (tierRank || 0);
if (name.includes("vct")) price *= 1.25;
if (name.includes("champions")) price *= 2;
return price / 124;
}
export function calculateMaxPrice(basePrice, skin) {
let res = basePrice;
res *= (1 + (skin.levels.length / Math.max(skin.levels.length, 2)));
res *= (1 + (skin.chromas.length / 4));
res *= 1 + skin.levels.length / Math.max(skin.levels.length, 2);
res *= 1 + skin.chromas.length / 4;
return res;
}
function formatTierText(rank, displayName) {
const tiers = {
0: '**<:select:1362964319498670222> Select**',
1: '**<:deluxe:1362964308094488797> Deluxe**',
2: '**<:premium:1362964330349330703> Premium**',
3: '**<:exclusive:1362964427556651098> Exclusive**',
4: '**<:ultra:1362964339685986314> Ultra**',
0: "**<:select:1362964319498670222> Select**",
1: "**<:deluxe:1362964308094488797> Deluxe**",
2: "**<:premium:1362964330349330703> Premium**",
3: "**<:exclusive:1362964427556651098> Exclusive**",
4: "**<:ultra:1362964339685986314> Ultra**",
};
let res = tiers[rank] || 'Pas de tier';
if (displayName.includes('VCT')) res += ' | Esports';
if (displayName.toLowerCase().includes('champions')) res += ' | Champions';
let res = tiers[rank] || "Pas de tier";
if (displayName.includes("VCT")) res += " | Esports";
if (displayName.toLowerCase().includes("champions")) res += " | Champions";
return res;
}