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"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <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> </profile>
</component> </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 # FLOPOBOT DEUXIEME DU NOM
## Project structure ## Project structure
Below is a basic overview of the 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 ## FlopoSite
- FlopoBot has its own website to use it a different way - FlopoBot has its own website to use it a different way
[FlopoSite's repo](https://github.com/cassoule/floposite) [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(); 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 "dotenv/config";
import http from 'http'; import http from "http";
import { Server } from 'socket.io'; import { Server } from "socket.io";
import { app } from './src/server/app.js'; import { app } from "./src/server/app.js";
import { client } from './src/bot/client.js'; import { client } from "./src/bot/client.js";
import { initializeEvents } from './src/bot/events.js'; import { initializeEvents } from "./src/bot/events.js";
import { initializeSocket } from './src/server/socket.js'; import { initializeSocket } from "./src/server/socket.js";
import { getAkhys, setupCronJobs } from './src/utils/index.js'; import { setupCronJobs } from "./src/utils/index.js";
// --- SERVER INITIALIZATION --- // --- SERVER INITIALIZATION ---
const PORT = process.env.PORT || 25578; const PORT = process.env.PORT || 25578;
const server = http.createServer(app); const server = http.createServer(app);
// --- SOCKET.IO INITIALIZATION --- // --- 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, { export const io = new Server(server, {
cors: { cors: {
origin: FLAPI_URL, origin: FLAPI_URL,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'], methods: ["GET", "POST", "PUT", "OPTIONS"],
}, },
pingInterval: 5000, pingInterval: 5000,
pingTimeout: 5000, pingTimeout: 5000,
}); });
initializeSocket(io, client); initializeSocket(io, client);
// --- BOT INITIALIZATION --- // --- BOT INITIALIZATION ---
initializeEvents(client, io); initializeEvents(client, io);
client.login(process.env.BOT_TOKEN).then(() => { client.login(process.env.BOT_TOKEN).then(() => {
console.log(`Logged in as ${client.user.tag}`); console.log(`Logged in as ${client.user.tag}`);
console.log('[Discord Bot Events Initialized]'); console.log("[Discord Bot Events Initialized]");
}); });
// --- APP STARTUP --- // --- APP STARTUP ---
server.listen(PORT, async () => { server.listen(PORT, async () => {
console.log(`Express+Socket.IO server listening on port ${PORT}`); console.log(`Express+Socket.IO server listening on port ${PORT}`);
console.log(`[Connected with ${FLAPI_URL}]`); console.log(`[Connected with ${FLAPI_URL}]`);
// Initial data fetch and setup // Initial data fetch and setup
try { /*try {
await getAkhys(client); await getAkhys(client);
} catch (error) { } catch (error) {
console.log('Initial Fetch Error'); console.log('Initial Fetch Error');
} }*/
// Setup scheduled tasks // Setup scheduled tasks
//setupCronJobs(client, io); setupCronJobs(client, io);
console.log('[Cron Jobs Initialized]'); 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" "uuid": "^11.1.0"
}, },
"devDependencies": { "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", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["config:recommended", ":disableDependencyDashboard", ":preserveSemverRanges"],
"config:recommended", "ignorePaths": ["**/node_modules/**"]
":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. * A generic function for making requests to the Discord API.
@@ -11,7 +11,7 @@ import 'dotenv/config';
*/ */
export async function DiscordRequest(endpoint, options) { export async function DiscordRequest(endpoint, options) {
// Construct the full API URL // 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 // Stringify the payload if it exists
if (options && options.body) { if (options && options.body) {
@@ -21,16 +21,16 @@ export async function DiscordRequest(endpoint, options) {
// Use fetch to make the request, automatically including required headers // Use fetch to make the request, automatically including required headers
const res = await fetch(url, { const res = await fetch(url, {
headers: { headers: {
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`, Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
'Content-Type': 'application/json; charset=UTF-8', "Content-Type": "application/json; charset=UTF-8",
'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)', "User-Agent": "DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)",
}, },
...options, // Spread the given options (e.g., method, body) ...options, // Spread the given options (e.g., method, body)
}); });
// If the request was not successful, throw a detailed error // If the request was not successful, throw a detailed error
if (!res.ok) { if (!res.ok) {
let data let data;
try { try {
data = await res.json(); data = await res.json();
} catch (err) { } catch (err) {
@@ -54,12 +54,12 @@ export async function InstallGlobalCommands(appId, commands) {
// API endpoint for bulk overwriting global commands // API endpoint for bulk overwriting global commands
const endpoint = `applications/${appId}/commands`; const endpoint = `applications/${appId}/commands`;
console.log('Installing global commands...'); console.log("Installing global commands...");
try { try {
// This uses the generic DiscordRequest function to make the API call // This uses the generic DiscordRequest function to make the API call
await DiscordRequest(endpoint, { method: 'PUT', body: commands }); await DiscordRequest(endpoint, { method: "PUT", body: commands });
console.log('Successfully installed global commands.'); console.log("Successfully installed global commands.");
} catch (err) { } 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') { export async function getValorantSkins(locale = "fr-FR") {
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' }); const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: "GET" });
const data = await response.json(); const data = await response.json();
return data.data return data.data;
} }
export async function getSkinTiers(locale='fr-FR') { export async function getSkinTiers(locale = "fr-FR") {
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'}); const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: "GET" });
const data = await response.json(); 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. * The single, shared Discord.js Client instance for the entire application.

View File

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

View File

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

View File

@@ -3,9 +3,9 @@ import {
MessageComponentTypes, MessageComponentTypes,
ButtonStyleTypes, ButtonStyleTypes,
InteractionResponseFlags, InteractionResponseFlags,
} from 'discord-interactions'; } from "discord-interactions";
import { activeInventories, skins } from '../../game/state.js'; import { activeInventories, skins } from "../../game/state.js";
import { getUserInventory } from '../../database/index.js'; import { getUserInventory } from "../../database/index.js";
/** /**
* Handles the /inventory slash command. * Handles the /inventory slash command.
@@ -33,11 +33,13 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
embeds: [{ embeds: [
{
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
description: "Cet inventaire est vide.", 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) => { const getChromaText = (skin, skinInfo) => {
let result = ""; let result = "";
for (let i = 1; i <= skinInfo.chromas.length; i++) { 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) => { const getChromaName = (skin, skinInfo) => {
if (skin.currentChroma > 1) { 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); const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
return match ? match[1].trim() : name; return match ? match[1].trim() : name;
} }
return 'Base'; return "Base";
}; };
const getImageUrl = (skin, skinInfo) => { const getImageUrl = (skin, skinInfo) => {
@@ -91,17 +96,28 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
// --- 5. Build Initial Components (Buttons) --- // --- 5. Build Initial Components (Buttons) ---
const components = [ 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 // Only show upgrade button if the skin is upgradable AND the command user owns the inventory
if (isUpgradable && targetUserId === commandUserId) { if (isUpgradable && targetUserId === commandUserId) {
components.push({ components.push({
type: MessageComponentTypes.BUTTON, type: MessageComponentTypes.BUTTON,
custom_id: `upgrade_${interactionId}`, custom_id: `upgrade_${interactionId}`,
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`, label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
style: ButtonStyleTypes.PRIMARY, style: ButtonStyleTypes.PRIMARY,
}); });
} }
@@ -110,29 +126,40 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
embeds: [{ embeds: [
{
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
footer: { text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` }, footer: {
fields: [{ text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
},
fields: [
{
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
}], },
],
image: { url: getImageUrl(currentSkin, skinData) }, 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, type: MessageComponentTypes.BUTTON,
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
label: 'Voir sur FlopoSite', label: "Voir sur FlopoSite",
style: ButtonStyleTypes.LINK,}] style: ButtonStyleTypes.LINK,
}], },
],
},
],
}, },
}); });
} catch (error) { } catch (error) {
console.error('Error handling /inventory command:', error); console.error("Error handling /inventory command:", error);
return res.status(500).json({ error: 'Failed to generate inventory.' }); return res.status(500).json({ error: "Failed to generate inventory." });
} }
} }

View File

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

View File

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

View File

@@ -3,13 +3,13 @@ import {
InteractionResponseFlags, InteractionResponseFlags,
MessageComponentTypes, MessageComponentTypes,
ButtonStyleTypes, ButtonStyleTypes,
} from 'discord-interactions'; } from "discord-interactions";
import { formatTime, getOnlineUsersWithRole } from '../../utils/index.js'; import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
import { DiscordRequest } from '../../api/discord.js'; import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from '../../game/state.js'; import { activePolls } from "../../game/state.js";
import { getSocketIo } from '../../server/socket.js'; import { getSocketIo } from "../../server/socket.js";
import { getUser } from '../../database/index.js'; import { getUser } from "../../database/index.js";
/** /**
* Handles the /timeout slash command. * Handles the /timeout slash command.
@@ -34,7 +34,7 @@ export async function handleTimeoutCommand(req, res, client) {
// --- Validation Checks --- // --- Validation Checks ---
// 1. Check if a poll is already running for the target user // 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) { if (existingPoll) {
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, 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 onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
const requiredMajority = Math.max( const requiredMajority = Math.max(
parseInt(process.env.MIN_VOTES, 10), 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 // Store poll data in the active state
@@ -102,109 +102,141 @@ export async function handleTimeoutCommand(req, res, client) {
if (remaining === 0) { if (remaining === 0) {
clearInterval(countdownInterval); clearInterval(countdownInterval);
const votersList = poll.voters.map(voterId => { const votersList = poll.voters
.map((voterId) => {
const user = getUser.get(voterId); const user = getUser.get(voterId);
return `- ${user?.globalName || 'Utilisateur Inconnu'}`; return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}).join('\n'); })
.join("\n");
try { try {
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
{
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`, title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
description: `Il manquait **${votesNeeded}** vote(s).`, description: `Il manquait **${votesNeeded}** vote(s).`,
fields: [{ fields: [
name: 'Pour', {
name: "Pour",
value: `${poll.for}\n${votersList}`, value: `${poll.for}\n${votersList}`,
inline: true, inline: true,
}], },
color: 0xFF4444, // Red for failure ],
}], color: 0xff4444, // Red for failure
},
],
components: [], // Remove buttons components: [], // Remove buttons
}, },
}); });
} catch (err) { } 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 // Clean up the poll from active state
delete activePolls[pollId]; delete activePolls[pollId];
io.emit('poll-update'); // Notify frontend io.emit("poll-update"); // Notify frontend
return; return;
} }
// --- Periodic Update Logic --- // --- Periodic Update Logic ---
// Update the message every second with the new countdown // Update the message every second with the new countdown
try { try {
const votersList = poll.voters.map(voterId => { const votersList = poll.voters
.map((voterId) => {
const user = getUser.get(voterId); const user = getUser.get(voterId);
return `- ${user?.globalName || 'Utilisateur Inconnu'}`; return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}).join('\n'); })
.join("\n");
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
title: 'Vote de Timeout', {
title: "Vote de Timeout",
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
fields: [{ fields: [
name: 'Pour', {
name: "Pour",
value: `${poll.for}\n${votersList}`, value: `${poll.for}\n${votersList}`,
inline: true, inline: true,
}, { },
name: 'Temps restant', {
name: "Temps restant",
value: `${countdownText}`, value: `${countdownText}`,
inline: false, inline: false,
}], },
color: 0x5865F2, // Discord Blurple ],
}], color: 0x5865f2, // Discord Blurple
},
],
// Keep the components so people can still vote // Keep the components so people can still vote
components: [{ components: [
{
type: MessageComponentTypes.ACTION_ROW, type: MessageComponentTypes.ACTION_ROW,
components: [ 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) { } 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 the message was deleted, stop trying to update it.
if (err.message.includes('Unknown Message')) { if (err.message.includes("Unknown Message")) {
clearInterval(countdownInterval); clearInterval(countdownInterval);
delete activePolls[pollId]; delete activePolls[pollId];
io.emit('poll-update'); io.emit("poll-update");
} }
} }
}, 2000); // Update every 2 seconds to avoid rate limits }, 2000); // Update every 2 seconds to avoid rate limits
// --- Send Initial Response --- // --- Send Initial Response ---
io.emit('poll-update'); // Notify frontend io.emit("poll-update"); // Notify frontend
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
embeds: [{ embeds: [
title: 'Vote de Timeout', {
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).`, description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
fields: [{ fields: [
name: 'Pour', {
value: '✅ 0', name: "Pour",
value: "✅ 0",
inline: true, inline: true,
}, { },
name: 'Temps restant', {
name: "Temps restant",
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`, value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
inline: false, inline: false,
}], },
color: 0x5865F2, ],
}], color: 0x5865f2,
components: [{ },
],
components: [
{
type: MessageComponentTypes.ACTION_ROW, type: MessageComponentTypes.ACTION_ROW,
components: [ 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 { import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
InteractionResponseType, import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
InteractionResponseFlags,
} from 'discord-interactions';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import { postAPOBuy } from '../../utils/index.js'; import { postAPOBuy } from "../../utils/index.js";
import { DiscordRequest } from '../../api/discord.js'; import { DiscordRequest } from "../../api/discord.js";
import {getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js'; import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
import { skins } from '../../game/state.js'; import { skins } from "../../game/state.js";
/** /**
* Handles the /valorant slash command for opening a "skin case". * Handles the /valorant slash command for opening a "skin case".
@@ -47,7 +44,7 @@ export async function handleValorantCommand(req, res, client) {
insertLog.run({ insertLog.run({
id: `${userId}-${Date.now()}`, id: `${userId}-${Date.now()}`,
user_id: userId, user_id: userId,
action: 'VALO_CASE_OPEN', action: "VALO_CASE_OPEN",
target_user_id: null, target_user_id: null,
coins_amount: -valoPrice, coins_amount: -valoPrice,
user_new_amount: commandUser.coins - valoPrice, user_new_amount: commandUser.coins - valoPrice,
@@ -55,21 +52,20 @@ export async function handleValorantCommand(req, res, client) {
updateUserCoins.run({ updateUserCoins.run({
id: userId, id: userId,
coins: commandUser.coins - valoPrice, coins: commandUser.coins - valoPrice,
}) });
// --- 2. Send Initial "Opening" Response --- // --- 2. Send Initial "Opening" Response ---
// Acknowledge the interaction immediately with a loading message. // Acknowledge the interaction immediately with a loading message.
const initialEmbed = new EmbedBuilder() const initialEmbed = new EmbedBuilder()
.setTitle('Ouverture de la caisse...') .setTitle("Ouverture de la caisse...")
.setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif') .setImage("https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif")
.setColor('#F2F3F3'); .setColor("#F2F3F3");
await res.send({ await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { embeds: [initialEmbed] }, data: { embeds: [initialEmbed] },
}); });
// --- 3. Run the skin reveal logic after a delay --- // --- 3. Run the skin reveal logic after a delay ---
setTimeout(async () => { setTimeout(async () => {
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
@@ -96,8 +92,8 @@ export async function handleValorantCommand(req, res, client) {
// --- Calculate Price --- // --- Calculate Price ---
const calculatePrice = () => { const calculatePrice = () => {
let result = parseFloat(dbSkin.basePrice); let result = parseFloat(dbSkin.basePrice);
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2))); result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= (1 + (randomChroma / 4)); result *= 1 + randomChroma / 4;
return parseFloat(result.toFixed(0)); return parseFloat(result.toFixed(0));
}; };
const finalPrice = calculatePrice(); const finalPrice = calculatePrice();
@@ -117,30 +113,29 @@ export async function handleValorantCommand(req, res, client) {
// --- Edit the Original Message with the Result --- // --- Edit the Original Message with the Result ---
await DiscordRequest(webhookEndpoint, { await DiscordRequest(webhookEndpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [finalEmbed], embeds: [finalEmbed],
components: components, components: components,
}, },
}); });
} catch (revealError) { } catch (revealError) {
console.error('Error during skin reveal:', revealError); console.error("Error during skin reveal:", revealError);
// Inform the user that something went wrong // Inform the user that something went wrong
await DiscordRequest(webhookEndpoint, { await DiscordRequest(webhookEndpoint, {
method: 'PATCH', method: "PATCH",
body: { 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: [], embeds: [],
}, },
}); });
} }
}, 5000); // 5-second delay for suspense }, 5000); // 5-second delay for suspense
} catch (error) { } 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. // 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 = () => { const getChromaName = () => {
if (chroma > 1) { 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); 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 = () => { const getImageUrl = () => {
@@ -167,14 +165,15 @@ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
return levelData?.displayIcon || skinData.displayIcon; return levelData?.displayIcon || skinData.displayIcon;
}; };
const lvlText = (level >= 1 ? '1⃣' : '') + const lvlText =
(level >= 2 ? '2️⃣' : '') + (level >= 1 ? "1️⃣" : "") +
(level >= 3 ? '3️⃣' : '') + (level >= 2 ? "2️⃣" : "") +
(level >= 4 ? '4️⃣' : '') + (level >= 3 ? "3️⃣" : "") +
(level >= 5 ? '5️⃣' : '') + (level >= 4 ? "4️⃣" : "") +
(level >= 6 ? '6️⃣' : '') + (level >= 5 ? "5️⃣" : "") +
'◾'.repeat(skinData.levels.length - level); (level >= 6 ? "6⃣" : "") +
const chromaText = '💠'.repeat(chroma) + '◾'.repeat(skinData.chromas.length - chroma); "◾".repeat(skinData.levels.length - level);
const chromaText = "💠".repeat(chroma) + "◾".repeat(skinData.chromas.length - chroma);
return new EmbedBuilder() return new EmbedBuilder()
.setTitle(`${skinData.displayName} | ${getChromaName()}`) .setTitle(`${skinData.displayName} | ${getChromaName()}`)
@@ -182,11 +181,15 @@ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
.setColor(`#${dbSkin.tierColor}`) .setColor(`#${dbSkin.tierColor}`)
.setImage(getImageUrl()) .setImage(getImageUrl())
.setFields([ .setFields([
{ name: 'Lvl', value: lvlText || 'N/A', inline: true }, { name: "Lvl", value: lvlText || "N/A", inline: true },
{ name: 'Chroma', value: chromaText || 'N/A', inline: true }, { name: "Chroma", value: chromaText || "N/A", inline: true },
{ name: 'Prix', value: `**${price}** <:vp:1362964205808128122>`, 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. */ /** Builds the action row with a video button if a video is available. */
@@ -203,11 +206,8 @@ function buildComponents(skinData, level, chroma) {
if (videoUrl) { if (videoUrl) {
return [ return [
new ActionRowBuilder().addComponents( new ActionRowBuilder().addComponents(
new ButtonBuilder() new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl),
.setLabel('🎬 Aperçu Vidéo') ),
.setStyle(ButtonStyle.Link)
.setURL(videoUrl)
)
]; ];
} }
return []; // Return an empty array if no video is available return []; // Return an empty array if no video is available

View File

@@ -3,10 +3,10 @@ import {
MessageComponentTypes, MessageComponentTypes,
ButtonStyleTypes, ButtonStyleTypes,
InteractionResponseFlags, InteractionResponseFlags,
} from 'discord-interactions'; } from "discord-interactions";
import { DiscordRequest } from '../../api/discord.js'; import { DiscordRequest } from "../../api/discord.js";
import { activeInventories, skins } from '../../game/state.js'; import { activeInventories, skins } from "../../game/state.js";
/** /**
* Handles navigation button clicks (Previous/Next) for the inventory embed. * 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; const { custom_id } = data;
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id // 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 --- // --- 1. Retrieve the interactive session ---
const inventorySession = activeInventories[interactionId]; const inventorySession = activeInventories[interactionId];
@@ -46,16 +46,14 @@ export async function handleInventoryNav(req, res, client) {
}); });
} }
// --- 3. Update Page Number --- // --- 3. Update Page Number ---
const { amount } = inventorySession; const { amount } = inventorySession;
if (direction === 'next') { if (direction === "next") {
inventorySession.page = (inventorySession.page + 1) % amount; inventorySession.page = (inventorySession.page + 1) % amount;
} else if (direction === 'prev') { } else if (direction === "prev") {
inventorySession.page = (inventorySession.page - 1 + amount) % amount; inventorySession.page = (inventorySession.page - 1 + amount) % amount;
} }
try { try {
// --- 4. Rebuild Embed with New Page Content --- // --- 4. Rebuild Embed with New Page Content ---
const { page, inventorySkins } = inventorySession; const { page, inventorySkins } = inventorySession;
@@ -73,18 +71,21 @@ export async function handleInventoryNav(req, res, client) {
const getChromaText = (skin, skinInfo) => { const getChromaText = (skin, skinInfo) => {
let result = ""; let result = "";
for (let i = 1; i <= skinInfo.chromas.length; i++) { 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) => { const getChromaName = (skin, skinInfo) => {
if (skin.currentChroma > 1) { 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); const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
return match ? match[1].trim() : name; return match ? match[1].trim() : name;
} }
return 'Base'; return "Base";
}; };
const getImageUrl = (skin, skinInfo) => { const getImageUrl = (skin, skinInfo) => {
@@ -98,43 +99,66 @@ export async function handleInventoryNav(req, res, client) {
// --- 5. Rebuild Components (Buttons) --- // --- 5. Rebuild Components (Buttons) ---
let components = [ 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 // Conditionally add the upgrade button
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) { if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
components.push({ components.push({
type: MessageComponentTypes.BUTTON, type: MessageComponentTypes.BUTTON,
custom_id: `upgrade_${interactionId}`, custom_id: `upgrade_${interactionId}`,
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`, label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
style: ButtonStyleTypes.PRIMARY, style: ButtonStyleTypes.PRIMARY,
}); });
} }
// --- 6. Send PATCH Request to Update the Message --- // --- 6. Send PATCH Request to Update the Message ---
await DiscordRequest(inventorySession.endpoint, { await DiscordRequest(inventorySession.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
{
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
footer: { text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` }, footer: {
fields: [{ text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
},
fields: [
{
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
}], },
],
image: { url: getImageUrl(currentSkin, skinData) }, 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, type: MessageComponentTypes.BUTTON,
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
label: 'Voir sur FlopoSite', label: "Voir sur FlopoSite",
style: ButtonStyleTypes.LINK,}] 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, // This tells Discord the interaction was received, and since the message is already updated,
// no further action is needed. // no further action is needed.
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
} catch (error) { } 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. // In case of an error, we should still acknowledge the interaction to prevent it from failing.
// We can send a silent, ephemeral error message. // We can send a silent, ephemeral error message.
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { 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, flags: InteractionResponseFlags.EPHEMERAL,
} },
}); });
} }
} }

View File

@@ -1,11 +1,8 @@
import { import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
InteractionResponseType, import { DiscordRequest } from "../../api/discord.js";
InteractionResponseFlags, import { activePolls } from "../../game/state.js";
} from 'discord-interactions'; import { getSocketIo } from "../../server/socket.js";
import { DiscordRequest } from '../../api/discord.js'; import { getUser } from "../../database/index.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. * 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; const { custom_id } = data;
// --- 1. Parse Component ID --- // --- 1. Parse Component ID ---
const [_, voteType, pollId] = custom_id.split('_'); // e.g., ['vote', 'for', '12345...'] const [_, voteType, pollId] = custom_id.split("_"); // e.g., ['vote', 'for', '12345...']
const isVotingFor = voteType === 'for'; const isVotingFor = voteType === "for";
// --- 2. Retrieve Poll and Validate --- // --- 2. Retrieve Poll and Validate ---
const poll = activePolls[pollId]; const poll = activePolls[pollId];
@@ -62,7 +59,7 @@ export async function handlePollVote(req, res) {
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
content: 'Vous avez déjà voté pour ce sondage.', content: "Vous avez déjà voté pour ce sondage.",
flags: InteractionResponseFlags.EPHEMERAL, flags: InteractionResponseFlags.EPHEMERAL,
}, },
}); });
@@ -76,10 +73,9 @@ export async function handlePollVote(req, res) {
poll.against++; poll.against++;
} }
io.emit('poll-update'); // Notify frontend clients of the change io.emit("poll-update"); // Notify frontend clients of the change
const votersList = poll.voters.map(vId => `- ${getUser.get(vId)?.globalName || 'Utilisateur Inconnu'}`).join('\n');
const votersList = poll.voters.map((vId) => `- ${getUser.get(vId)?.globalName || "Utilisateur Inconnu"}`).join("\n");
// --- 4. Check for Majority --- // --- 4. Check for Majority ---
if (isVotingFor && poll.for >= poll.requiredMajority) { if (isVotingFor && poll.for >= poll.requiredMajority) {
@@ -88,19 +84,27 @@ export async function handlePollVote(req, res) {
// a. Update the poll message to show success // a. Update the poll message to show success
try { try {
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
title: 'Vote Terminé - Timeout Appliqué !', {
title: "Vote Terminé - Timeout Appliqué !",
description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`, 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 }], fields: [
color: 0x22A55B, // Green for success {
}], name: "Votes Pour",
value: `${poll.for}\n${votersList}`,
inline: true,
},
],
color: 0x22a55b, // Green for success
},
],
components: [], // Remove buttons components: [], // Remove buttons
}, },
}); });
} catch (err) { } 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 // 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 timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`; const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
await DiscordRequest(endpointTimeout, { await DiscordRequest(endpointTimeout, {
method: 'PATCH', method: "PATCH",
body: { communication_disabled_until: timeoutUntil }, body: { communication_disabled_until: timeoutUntil },
}); });
// c. Send a public confirmation message and clean up // c. Send a public confirmation message and clean up
delete activePolls[pollId]; delete activePolls[pollId];
io.emit('poll-update'); io.emit("poll-update");
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`, content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`,
}, },
}); });
} catch (err) { } catch (err) {
console.error('Error timing out user:', err); console.error("Error timing out user:", err);
delete activePolls[pollId]; delete activePolls[pollId];
io.emit('poll-update'); io.emit("poll-update");
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
@@ -140,7 +143,7 @@ export async function handlePollVote(req, res) {
res.send({ res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { data: {
content: 'Votre vote a été enregistré ! ✅', content: "Votre vote a été enregistré ! ✅",
flags: InteractionResponseFlags.EPHEMERAL, flags: InteractionResponseFlags.EPHEMERAL,
}, },
}); });
@@ -152,25 +155,30 @@ export async function handlePollVote(req, res) {
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`; const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
DiscordRequest(poll.endpoint, { DiscordRequest(poll.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
title: 'Vote de Timeout', {
title: "Vote de Timeout",
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
fields: [{ fields: [
name: 'Pour', {
name: "Pour",
value: `${poll.for}\n${votersList}`, value: `${poll.for}\n${votersList}`,
inline: true, inline: true,
}, { },
name: 'Temps restant', {
name: "Temps restant",
value: `${countdownText}`, value: `${countdownText}`,
inline: false, inline: false,
}], },
color: 0x5865F2, ],
}], color: 0x5865f2,
},
],
// Keep the original components so people can still vote // Keep the original components so people can still vote
components: req.body.message.components, 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, InteractionResponseFlags,
MessageComponentTypes, MessageComponentTypes,
ButtonStyleTypes, ButtonStyleTypes,
} from 'discord-interactions'; } from "discord-interactions";
import { DiscordRequest } from '../../api/discord.js'; import { DiscordRequest } from "../../api/discord.js";
import { activeSearchs, skins } from '../../game/state.js'; import { activeSearchs, skins } from "../../game/state.js";
/** /**
* Handles navigation button clicks (Previous/Next) for the search results embed. * 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; const { custom_id } = data;
// Extract direction and the original interaction ID from the custom_id // 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 --- // --- 1. Retrieve the interactive session ---
const searchSession = activeSearchs[interactionId]; const searchSession = activeSearchs[interactionId];
@@ -48,9 +48,9 @@ export async function handleSearchNav(req, res, client) {
// --- 3. Update Page Number --- // --- 3. Update Page Number ---
const { amount } = searchSession; const { amount } = searchSession;
if (direction === 'next') { if (direction === "next") {
searchSession.page = (searchSession.page + 1) % amount; searchSession.page = (searchSession.page + 1) % amount;
} else if (direction === 'prev') { } else if (direction === "prev") {
searchSession.page = (searchSession.page - 1 + amount) % amount; 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 // Fetch owner details if the skin is owned
let ownerText = ''; let ownerText = "";
if (currentSkin.user_id) { if (currentSkin.user_id) {
try { try {
const owner = await client.users.fetch(currentSkin.user_id); const owner = await client.users.fetch(currentSkin.user_id);
ownerText = `| **@${owner.globalName || owner.username}** ✅`; ownerText = `| **@${owner.globalName || owner.username}** ✅`;
} catch (e) { } catch (e) {
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); 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 --- // --- 5. Send PATCH Request to Update the Message ---
// Note: The components (buttons) do not change, so we can reuse them from the original message. // Note: The components (buttons) do not change, so we can reuse them from the original message.
await DiscordRequest(searchSession.endpoint, { await DiscordRequest(searchSession.endpoint, {
method: 'PATCH', method: "PATCH",
body: { body: {
embeds: [{ embeds: [
title: 'Résultats de la recherche', {
title: "Résultats de la recherche",
description: `🔎 _"${searchValue}"_`, description: `🔎 _"${searchValue}"_`,
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
fields: [{ fields: [
{
name: `**${currentSkin.displayName}**`, name: `**${currentSkin.displayName}**`,
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`, value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
}], },
],
image: { url: getImageUrl(skinData) }, image: { url: getImageUrl(skinData) },
footer: { text: `Résultat ${page + 1}/${amount}` }, footer: { text: `Résultat ${page + 1}/${amount}` },
}], },
],
components: req.body.message.components, // Reuse existing components components: req.body.message.components, // Reuse existing components
}, },
}); });
// --- 6. Acknowledge the Interaction --- // --- 6. Acknowledge the Interaction ---
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
} catch (error) { } catch (error) {
console.error('Error handling search navigation:', error); console.error("Error handling search navigation:", error);
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { 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, flags: InteractionResponseFlags.EPHEMERAL,
} },
}); });
} }
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { handleMessageCreate } from './handlers/messageCreate.js'; import { handleMessageCreate } from "./handlers/messageCreate.js";
import { getAkhys, setupCronJobs } from '../utils/index.js'; import { getAkhys, setupCronJobs } from "../utils/index.js";
/** /**
* Initializes and attaches all necessary event listeners to the Discord client. * Initializes and attaches all necessary event listeners to the Discord client.
@@ -12,19 +12,19 @@ export function initializeEvents(client, io) {
// --- on 'ready' --- // --- on 'ready' ---
// This event fires once the bot has successfully logged in and is ready to operate. // 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. // 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(`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); await getAkhys(client);
console.log('[Startup] Setting up scheduled tasks...'); console.log("[Startup] Setting up scheduled tasks...");
setupCronJobs(client, io); setupCronJobs(client, io);
console.log('--- FlopoBOT is fully operational ---'); console.log("--- FlopoBOT is fully operational ---");
}); });
// --- on 'messageCreate' --- // --- on 'messageCreate' ---
// This event fires every time a message is sent in a channel the bot can see. // 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. // 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 // 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. // without needing to import them, preventing potential circular dependencies.
await handleMessageCreate(message, client, io); await handleMessageCreate(message, client, io);

View File

@@ -1,22 +1,19 @@
import { import { InteractionType, InteractionResponseType } from "discord-interactions";
InteractionType,
InteractionResponseType,
} from 'discord-interactions';
// --- Command Handlers --- // --- Command Handlers ---
import { handleTimeoutCommand } from '../commands/timeout.js'; import { handleTimeoutCommand } from "../commands/timeout.js";
import { handleInventoryCommand } from '../commands/inventory.js'; import { handleInventoryCommand } from "../commands/inventory.js";
import { handleValorantCommand } from '../commands/valorant.js'; import { handleValorantCommand } from "../commands/valorant.js";
import { handleInfoCommand } from '../commands/info.js'; import { handleInfoCommand } from "../commands/info.js";
import { handleSkinsCommand } from '../commands/skins.js'; import { handleSkinsCommand } from "../commands/skins.js";
import { handleSearchCommand } from '../commands/search.js'; import { handleSearchCommand } from "../commands/search.js";
import { handleFlopoSiteCommand } from '../commands/floposite.js'; import { handleFlopoSiteCommand } from "../commands/floposite.js";
// --- Component Handlers --- // --- Component Handlers ---
import { handlePollVote } from '../components/pollVote.js'; import { handlePollVote } from "../components/pollVote.js";
import { handleInventoryNav } from '../components/inventoryNav.js'; import { handleInventoryNav } from "../components/inventoryNav.js";
import { handleUpgradeSkin } from '../components/upgradeSkin.js'; import { handleUpgradeSkin } from "../components/upgradeSkin.js";
import { handleSearchNav } from '../components/searchNav.js'; import { handleSearchNav } from "../components/searchNav.js";
/** /**
* The main handler for all incoming interactions from Discord. * The main handler for all incoming interactions from Discord.
@@ -36,54 +33,53 @@ export async function handleInteraction(req, res, client) {
const { name } = data; const { name } = data;
switch (name) { switch (name) {
case 'timeout': case "timeout":
return await handleTimeoutCommand(req, res, client); return await handleTimeoutCommand(req, res, client);
case 'inventory': case "inventory":
return await handleInventoryCommand(req, res, client, id); return await handleInventoryCommand(req, res, client, id);
case 'valorant': case "valorant":
return await handleValorantCommand(req, res, client); return await handleValorantCommand(req, res, client);
case 'info': case "info":
return await handleInfoCommand(req, res, client); return await handleInfoCommand(req, res, client);
case 'skins': case "skins":
return await handleSkinsCommand(req, res, client); return await handleSkinsCommand(req, res, client);
case 'search': case "search":
return await handleSearchCommand(req, res, client, id); return await handleSearchCommand(req, res, client, id);
case 'floposite': case "floposite":
return await handleFlopoSiteCommand(req, res); return await handleFlopoSiteCommand(req, res);
default: default:
console.error(`Unknown command: ${name}`); 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) { if (type === InteractionType.MESSAGE_COMPONENT) {
const componentId = data.custom_id; const componentId = data.custom_id;
if (componentId.startsWith('vote_')) { if (componentId.startsWith("vote_")) {
return await handlePollVote(req, res, client); 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); return await handleInventoryNav(req, res, client);
} }
if (componentId.startsWith('upgrade_')) { if (componentId.startsWith("upgrade_")) {
return await handleUpgradeSkin(req, res, client); 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); return await handleSearchNav(req, res, client);
} }
// Fallback for other potential components // Fallback for other potential components
console.error(`Unknown component ID: ${componentId}`); 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 --- // --- Fallback for Unknown Interaction Types ---
console.error('Unknown interaction type:', type); console.error("Unknown interaction type:", type);
return res.status(400).json({ error: 'Unknown interaction type' }); return res.status(400).json({ error: "Unknown interaction type" });
} catch (error) { } 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 // 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 "dotenv/config";
import { getTimesChoices } from '../game/various.js'; import { getTimesChoices } from "../game/various.js";
import { capitalize, InstallGlobalCommands } from '../utils/index.js'; import { capitalize, InstallGlobalCommands } from "../utils/index.js";
function createTimesChoices() { function createTimesChoices() {
const choices = getTimesChoices(); const choices = getTimesChoices();
@@ -18,95 +18,103 @@ function createTimesChoices() {
// Timeout vote command // Timeout vote command
const TIMEOUT_COMMAND = { const TIMEOUT_COMMAND = {
name: 'timeout', name: "timeout",
description: 'Vote démocratique pour timeout un boug', description: "Vote démocratique pour timeout un boug",
options: [ options: [
{ {
type: 6, type: 6,
name: 'akhy', name: "akhy",
description: 'Qui ?', description: "Qui ?",
required: true, required: true,
}, },
{ {
type: 3, type: 3,
name: 'temps', name: "temps",
description: 'Combien de temps ?', description: "Combien de temps ?",
required: true, required: true,
choices: createTimesChoices(), choices: createTimesChoices(),
} },
], ],
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
// Valorant // Valorant
const VALORANT_COMMAND = { const VALORANT_COMMAND = {
name: 'valorant', name: "valorant",
description: `Ouvrir une caisse valorant (500 FlopoCoins)`, description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
// Own inventory command // Own inventory command
const INVENTORY_COMMAND = { const INVENTORY_COMMAND = {
name: 'inventory', name: "inventory",
description: 'Voir inventaire', description: "Voir inventaire",
options: [ options: [
{ {
type: 6, type: 6,
name: 'akhy', name: "akhy",
description: 'Qui ?', description: "Qui ?",
required: false, required: false,
}, },
], ],
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
const INFO_COMMAND = { const INFO_COMMAND = {
name: 'info', name: "info",
description: 'Qui est time out ?', description: "Qui est time out ?",
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
const SKINS_COMMAND = { const SKINS_COMMAND = {
name: 'skins', name: "skins",
description: 'Le top 10 des skins les plus chers.', description: "Le top 10 des skins les plus chers.",
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
const SITE_COMMAND = { const SITE_COMMAND = {
name: 'floposite', name: "floposite",
description: 'Lien vers FlopoSite', description: "Lien vers FlopoSite",
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], contexts: [0, 2],
} };
const SEARCH_SKIN_COMMAND = { const SEARCH_SKIN_COMMAND = {
name: 'search', name: "search",
description: 'Chercher un skin', description: "Chercher un skin",
options: [ options: [
{ {
type: 3, type: 3,
name: 'recherche', name: "recherche",
description: 'Tu cherches quoi ?', description: "Tu cherches quoi ?",
required: true, required: true,
}, },
], ],
type: 1, type: 1,
integration_types: [0, 1], integration_types: [0, 1],
contexts: [0, 2], 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() { export function registerCommands() {
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS); InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);

View File

@@ -1,10 +1,10 @@
import Database from "better-sqlite3"; import Database from "better-sqlite3";
export const flopoDB = new Database("flopobot.db");
export const flopoDB = new Database('flopobot.db');
export const stmtUsers = flopoDB.prepare(` export const stmtUsers = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users
(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
username TEXT NOT NULL, username TEXT NOT NULL,
globalName TEXT, globalName TEXT,
@@ -20,7 +20,8 @@ export const stmtUsers = flopoDB.prepare(`
`); `);
stmtUsers.run(); stmtUsers.run();
export const stmtSkins = flopoDB.prepare(` export const stmtSkins = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS skins ( CREATE TABLE IF NOT EXISTS skins
(
uuid TEXT PRIMARY KEY, uuid TEXT PRIMARY KEY,
displayName TEXT, displayName TEXT,
contentTierUuid TEXT, contentTierUuid TEXT,
@@ -36,44 +37,198 @@ export const stmtSkins = flopoDB.prepare(`
maxPrice INTEGER DEFAULT NULL 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 insertUser = flopoDB.prepare(
export const updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id'); `INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
export const updateUserAvatar = flopoDB.prepare('UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id'); VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`); );
export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0`); export const updateUser = flopoDB.prepare(
export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id'); `UPDATE users
export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?'); SET warned = @warned,
export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC'); warns = @warns,
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'); 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 insertSkin = flopoDB.prepare(
export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid'); `INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
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'); basePrice, currentLvl, currentChroma, currentPrice, maxPrice)
export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?'); VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC'); @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`,
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 updateSkin = flopoDB.prepare(
export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10'); `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) => { 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) => { 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) => { 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) => { 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(` export const stmtLogs = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS logs ( CREATE TABLE IF NOT EXISTS logs
(
id PRIMARY KEY, id PRIMARY KEY,
user_id TEXT REFERENCES users, user_id TEXT REFERENCES users,
action TEXT, action TEXT,
@@ -82,15 +237,18 @@ export const stmtLogs = flopoDB.prepare(`
user_new_amount INTEGER user_new_amount INTEGER
) )
`); `);
stmtLogs.run() 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 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(` export const stmtGames = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS games ( CREATE TABLE IF NOT EXISTS games
(
id PRIMARY KEY, id PRIMARY KEY,
p1 TEXT REFERENCES users, p1 TEXT REFERENCES users,
p2 TEXT REFERENCES users, p2 TEXT REFERENCES users,
@@ -104,31 +262,42 @@ export const stmtGames = flopoDB.prepare(`
timestamp TIMESTAMP timestamp TIMESTAMP
) )
`); `);
stmtGames.run() 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 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(` export const stmtElos = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS elos ( CREATE TABLE IF NOT EXISTS elos
(
id PRIMARY KEY REFERENCES users, id PRIMARY KEY REFERENCES users,
elo INTEGER elo INTEGER
) )
`); `);
stmtElos.run() stmtElos.run();
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) VALUES (@id, @elo)`); export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
export const getElos = flopoDB.prepare(`SELECT * FROM elos`); VALUES (@id, @elo)`);
export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`); export const getElos = flopoDB.prepare(`SELECT *
export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id'); 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(
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC') "SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
);
export const stmtSOTD = flopoDB.prepare(` export const stmtSOTD = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd ( CREATE TABLE IF NOT EXISTS sotd
(
id INT PRIMARY KEY, id INT PRIMARY KEY,
tableauPiles TEXT, tableauPiles TEXT,
foundationPiles TEXT, foundationPiles TEXT,
@@ -138,14 +307,21 @@ export const stmtSOTD = flopoDB.prepare(`
seed TEXT seed TEXT
) )
`); `);
stmtSOTD.run() stmtSOTD.run();
export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`) export const getSOTD = flopoDB.prepare(`SELECT *
export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`) FROM sotd
export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`) 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(` export const stmtSOTDStats = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd_stats ( CREATE TABLE IF NOT EXISTS sotd_stats
(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users, user_id TEXT REFERENCES users,
time INTEGER, time INTEGER,
@@ -153,38 +329,53 @@ export const stmtSOTDStats = flopoDB.prepare(`
score INTEGER 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 getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
export const getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`); FROM sotd_stats
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`); JOIN users ON users.id = sotd_stats.user_id
export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`); ORDER BY score DESC, moves ASC, time ASC`);
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`); 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() { export async function pruneOldLogs() {
const users = flopoDB.prepare(` const users = flopoDB
.prepare(
`
SELECT user_id SELECT user_id
FROM logs FROM logs
GROUP BY user_id GROUP BY user_id
HAVING COUNT(*) > ${process.env.LOGS_BY_USER} HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
`).all(); `,
)
.all();
const transaction = flopoDB.transaction(() => { const transaction = flopoDB.transaction(() => {
for (const { user_id } of users) { for (const { user_id } of users) {
flopoDB.prepare(` flopoDB
DELETE FROM logs .prepare(
WHERE id IN ( `
SELECT id FROM ( DELETE
SELECT id, FROM logs
WHERE id IN (SELECT id
FROM (SELECT id,
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
FROM logs 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

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

View File

@@ -1,12 +1,6 @@
import { import { getUser, getUserElo, insertElos, updateElo, insertGame } from "../database/index.js";
getUser, import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
getUserElo, import { client } from "../bot/client.js";
insertElos,
updateElo,
insertGame,
} from '../database/index.js';
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
import {client} from "../bot/client.js";
/** /**
* Handles Elo calculation for a standard 1v1 game. * Handles Elo calculation for a standard 1v1 game.
@@ -67,13 +61,17 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
const diff2 = finalP2Elo - p2CurrentElo; const diff2 = finalP2Elo - p2CurrentElo;
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle(`FlopoRank - ${type}`) .setTitle(`FlopoRank - ${type}`)
.setDescription(` .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 **${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'); `,
)
.setColor("#5865f2");
await generalChannel.send({ embeds: [embed] }); 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 --- // --- 4. Update Database ---
updateElo.run({ id: p1Id, elo: finalP1Elo }); 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 if (playerIds.length < 2) return; // Not enough players to calculate Elo
// Fetch all players' Elo data at once // Fetch all players' Elo data at once
const dbPlayers = playerIds.map(id => { const dbPlayers = playerIds.map((id) => {
const user = getUser.get(id); const user = getUser.get(id);
const elo = getUserElo.get({ id })?.elo || 1000; const elo = getUserElo.get({ id })?.elo || 1000;
return { ...user, elo }; return { ...user, elo };
@@ -120,7 +118,7 @@ export async function pokerEloHandler(room) {
const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount; 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 // 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)); 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)); const newElo = Math.max(100, Math.round(player.elo + eloChange));
if (!isNaN(newElo)) { 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 }); updateElo.run({ id: player.id, elo: newElo });
insertGame.run({ insertGame.run({
@@ -152,7 +152,7 @@ export async function pokerEloHandler(room) {
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
p1_new_elo: newElo, p1_new_elo: newElo,
p2_new_elo: null, p2_new_elo: null,
type: 'POKER_ROUND', type: "POKER_ROUND",
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {

View File

@@ -5,10 +5,12 @@ import {
getAllSkins, getAllSkins,
insertSOTD, insertSOTD,
clearSOTDStats, clearSOTDStats,
getAllSOTDStats, deleteSOTD, insertGame, getAllSOTDStats,
} from '../database/index.js'; deleteSOTD,
import { messagesTimestamps, activeSlowmodes, skins } from './state.js'; insertGame,
import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js'; } 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. * 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 // 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; return false;
} }
@@ -34,7 +36,7 @@ export async function channelPointsHandler(message) {
const userTimestamps = messagesTimestamps.get(author.id) || []; const userTimestamps = messagesTimestamps.get(author.id) || [];
// Filter out timestamps older than 15 minutes (900,000 ms) // 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 the user has already sent 10 messages in the last 15 mins, do nothing
if (recentTimestamps.length >= 10) { if (recentTimestamps.length >= 10) {
@@ -57,7 +59,7 @@ export async function channelPointsHandler(message) {
insertLog.run({ insertLog.run({
id: `${author.id}-${now}`, id: `${author.id}-${now}`,
user_id: author.id, user_id: author.id,
action: 'AUTO_COINS', action: "AUTO_COINS",
target_user_id: null, target_user_id: null,
coins_amount: coinsToAdd, coins_amount: coinsToAdd,
user_new_amount: newCoinTotal, 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) // 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 { try {
await message.delete(); await message.delete();
console.log(`Deleted a message from slowmoded user: ${author.username}`); console.log(`Deleted a message from slowmoded user: ${author.username}`);
@@ -112,12 +114,12 @@ export async function slowmodesHandler(message) {
*/ */
export function randomSkinPrice() { export function randomSkinPrice() {
const dbSkins = getAllSkins.all(); 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 randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid); const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
if (!randomSkinData) return '0.00'; if (!randomSkinData) return "0.00";
// Generate random level and chroma // Generate random level and chroma
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
@@ -128,8 +130,8 @@ export function randomSkinPrice() {
// Calculate price based on these random values // Calculate price based on these random values
let result = parseFloat(randomDbSkin.basePrice); let result = parseFloat(randomDbSkin.basePrice);
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2))); result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
result *= (1 + (randomChroma / 4)); result *= 1 + randomChroma / 4;
return result.toFixed(0); 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. * This function clears previous stats, awards the winner, and generates a new daily seed.
*/ */
export function initTodaysSOTD() { 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 // 1. Award previous day's winner
const rankings = getAllSOTDStats.all(); const rankings = getAllSOTDStats.all();
@@ -155,11 +157,13 @@ export function initTodaysSOTD() {
id: `${winnerId}-sotd-win-${Date.now()}`, id: `${winnerId}-sotd-win-${Date.now()}`,
target_user_id: null, target_user_id: null,
user_id: winnerId, user_id: winnerId,
action: 'SOTD_FIRST_PLACE', action: "SOTD_FIRST_PLACE",
coins_amount: reward, coins_amount: reward,
user_new_amount: newCoinTotal, 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({ insertGame.run({
id: `${winnerId}-${Date.now()}`, id: `${winnerId}-${Date.now()}`,
p1: winnerId, p1: winnerId,
@@ -170,7 +174,7 @@ export function initTodaysSOTD() {
p2_elo: null, p2_elo: null,
p1_new_elo: winnerUser.elo, p1_new_elo: winnerUser.elo,
p2_new_elo: null, p2_new_elo: null,
type: 'SOTD', type: "SOTD",
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -180,7 +184,7 @@ export function initTodaysSOTD() {
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2); const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
let numericSeed = 0; let numericSeed = 0;
for (let i = 0; i < newRandomSeed.length; i++) { for (let i = 0; i < newRandomSeed.length; i++) {
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF; numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xffffffff;
} }
const rng = createSeededRNG(numericSeed); const rng = createSeededRNG(numericSeed);
@@ -200,7 +204,7 @@ export function initTodaysSOTD() {
seed: newRandomSeed, seed: newRandomSeed,
}); });
console.log("Today's SOTD is ready with a new seed."); console.log("Today's SOTD is ready with a new seed.");
} catch(e) { } catch (e) {
console.error("Error saving new SOTD to database:", e); console.error("Error saving new SOTD to database:", e);
} }
} }

View File

@@ -1,12 +1,60 @@
import pkg from 'pokersolver'; import pkg from "pokersolver";
const { Hand } = pkg; const { Hand } = pkg;
// An array of all 52 standard playing cards. // An array of all 52 standard playing cards.
export const initialCards = [ export const initialCards = [
'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd', "Ad",
'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks', "2d",
'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc', "3d",
'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh', "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) --- // --- Scenario 1: Only one player left (everyone else folded) ---
if (activePlayers.length === 1) { 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 --- // --- Scenario 2: All remaining players are all-in ---
// The hand goes immediately to a "progressive showdown". // 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) { 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 --- // --- 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.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) { if (allBetsMatched) {
let nextPhase; let nextPhase;
switch (room.current_turn) { switch (room.current_turn) {
case 0: nextPhase = 'flop'; break; case 0:
case 1: nextPhase = 'turn'; break; nextPhase = "flop";
case 2: nextPhase = 'river'; break; break;
case 3: nextPhase = 'showdown'; break; case 1:
default: nextPhase = null; // Should not happen 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 }; return { endRound: true, winner: null, nextPhase: nextPhase };
} }
@@ -109,10 +171,10 @@ export function checkEndOfBettingRound(room) {
*/ */
export function checkRoomWinners(room) { export function checkRoomWinners(room) {
const communityCards = room.tapis; 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 // 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, id: player.id,
solution: Hand.solve([...communityCards, ...player.hand]), solution: Hand.solve([...communityCards, ...player.hand]),
})); }));
@@ -120,14 +182,17 @@ export function checkRoomWinners(room) {
if (playerSolutions.length === 0) return []; if (playerSolutions.length === 0) return [];
// Use pokersolver's `Hand.winners()` to find the best hand(s) // 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 // Find the player IDs that correspond to the winning hand solutions
const winnerIds = []; const winnerIds = [];
for (const winningHand of winningSolutions) { for (const winningHand of winningSolutions) {
for (const playerSol of playerSolutions) { for (const playerSol of playerSolutions) {
// Compare description and card pool to uniquely identify the hand // 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)) { if (!winnerIds.includes(playerSol.id)) {
winnerIds.push(playerSol.id); winnerIds.push(playerSol.id);
} }

View File

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

View File

@@ -44,7 +44,6 @@ export let activePredis = {};
// Format: { [userId]: { endAt, lastMessage } } // Format: { [userId]: { endAt, lastMessage } }
export let activeSlowmodes = {}; export let activeSlowmodes = {};
// --- Queues for Matchmaking --- // --- Queues for Matchmaking ---
// Stores user IDs waiting to play Tic-Tac-Toe. // Stores user IDs waiting to play Tic-Tac-Toe.
@@ -55,7 +54,6 @@ export let connect4Queue = [];
export let queueMessagesEndpoints = []; export let queueMessagesEndpoints = [];
// --- Rate Limiting and Caching --- // --- Rate Limiting and Caching ---
// Tracks message timestamps for the channel points system, keyed by user ID. // 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. // A predefined list of choices for the /timeout command's duration option.
const TimesChoices = [ const TimesChoices = [
{ name: '1 minute', value: 60 }, { name: "1 minute", value: 60 },
{ name: '5 minutes', value: 300 }, { name: "5 minutes", value: 300 },
{ name: '10 minutes', value: 600 }, { name: "10 minutes", value: 600 },
{ name: '15 minutes', value: 900 }, { name: "15 minutes", value: 900 },
{ name: '30 minutes', value: 1800 }, { name: "30 minutes", value: 1800 },
{ name: '1 heure', value: 3600 }, { name: "1 heure", value: 3600 },
{ name: '2 heures', value: 7200 }, { name: "2 heures", value: 7200 },
{ name: '3 heures', value: 10800 }, { name: "3 heures", value: 10800 },
{ name: '6 heures', value: 21600 }, { name: "6 heures", value: 21600 },
{ name: '9 heures', value: 32400 }, { name: "9 heures", value: 32400 },
{ name: '12 heures', value: 43200 }, { name: "12 heures", value: 43200 },
{ name: '16 heures', value: 57600 }, { name: "16 heures", value: 57600 },
{ name: '1 jour', value: 86400 }, { name: "1 jour", value: 86400 },
]; ];
/** /**
@@ -27,7 +27,6 @@ export function getTimesChoices() {
return TimesChoices; return TimesChoices;
} }
// --- Connect 4 Logic --- // --- Connect 4 Logic ---
/** /**
@@ -35,7 +34,9 @@ export function getTimesChoices() {
* @returns {Array<Array<null>>} A 2D array representing the board. * @returns {Array<Array<null>>} A 2D array representing the board.
*/ */
export function createConnect4Board() { 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 // Check horizontal
for (let r = 0; r < C4_ROWS; r++) { for (let r = 0; r < C4_ROWS; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) { 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) { if (
return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] }; 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 // Check vertical
for (let r = 0; r <= C4_ROWS - 4; r++) { for (let r = 0; r <= C4_ROWS - 4; r++) {
for (let c = 0; c < C4_COLS; c++) { 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) { if (
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] }; 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) // Check diagonal (down-right)
for (let r = 0; r <= C4_ROWS - 4; r++) { for (let r = 0; r <= C4_ROWS - 4; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) { 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) { if (
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}] }; 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) // Check diagonal (up-right)
for (let r = 3; r < C4_ROWS; r++) { for (let r = 3; r < C4_ROWS; r++) {
for (let c = 0; c <= C4_COLS - 4; c++) { 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) { if (
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}] }; 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) { export function checkConnect4Draw(board) {
// A draw occurs if the top row is completely full. // 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) { export function formatConnect4BoardForDiscord(board) {
const symbols = { const symbols = {
'R': '🔴', R: "🔴",
'Y': '🟡', Y: "🟡",
null: '⚪' // Using a white circle for empty slots 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 "dotenv/config";
import express from 'express'; import express from "express";
import { verifyKeyMiddleware } from 'discord-interactions'; import { verifyKeyMiddleware } from "discord-interactions";
import { handleInteraction } from '../bot/handlers/interactionCreate.js'; import { handleInteraction } from "../bot/handlers/interactionCreate.js";
import { client } from '../bot/client.js'; import { client } from "../bot/client.js";
// Import route handlers // Import route handlers
import { apiRoutes } from './routes/api.js'; import { apiRoutes } from "./routes/api.js";
import { pokerRoutes } from './routes/poker.js'; import { pokerRoutes } from "./routes/poker.js";
import { solitaireRoutes } from './routes/solitaire.js'; import { solitaireRoutes } from "./routes/solitaire.js";
import {getSocketIo} from "./socket.js"; import { getSocketIo } from "./socket.js";
import {erinyesRoutes} from "./routes/erinyes.js"; import { blackjackRoutes } from "./routes/blackjack.js";
import {blackjackRoutes} from "./routes/blackjack.js"; import { marketRoutes } from "./routes/market.js";
// --- EXPRESS APP INITIALIZATION --- // --- EXPRESS APP INITIALIZATION ---
const app = express(); const app = express();
const io = getSocketIo(); 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 --- // --- GLOBAL MIDDLEWARE ---
// CORS Middleware // CORS Middleware
app.use((req, res, next) => { app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', FLAPI_URL); 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-Headers",
"Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
);
next(); next();
}); });
// --- PRIMARY DISCORD INTERACTION ENDPOINT --- // --- PRIMARY DISCORD INTERACTION ENDPOINT ---
// This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.) // 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 // The actual logic is delegated to a dedicated handler for better organization
await handleInteraction(req, res, client); await handleInteraction(req, res, client);
}); });
@@ -37,24 +40,26 @@ app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re
app.use(express.json()); app.use(express.json());
// --- STATIC ASSETS --- // --- STATIC ASSETS ---
app.use('/public', express.static('public')); app.use("/public", express.static("public"));
// --- API ROUTES --- // --- API ROUTES ---
// General API routes (users, polls, etc.) // General API routes (users, polls, etc.)
app.use('/api', apiRoutes(client, io)); app.use("/api", apiRoutes(client, io));
// Poker-specific routes // Poker-specific routes
app.use('/api/poker', pokerRoutes(client, io)); app.use("/api/poker", pokerRoutes(client, io));
// Solitaire-specific routes // 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 // erinyes-specific routes
app.use('/api/erinyes', erinyesRoutes(client, io)); // app.use("/api/erinyes", erinyesRoutes(client, io));
export { app }; export { app };

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,18 +1,35 @@
import express from 'express'; import express from "express";
// --- Game Logic Imports --- // --- Game Logic Imports ---
import { import {
createDeck, shuffle, deal, isValidMove, moveCard, drawCard, createDeck,
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves shuffle,
} from '../../game/solitaire.js'; deal,
isValidMove,
moveCard,
drawCard,
checkWinCondition,
createSeededRNG,
seededShuffle,
undoMove,
draw3Cards,
checkAutoSolve,
autoSolveMoves,
} from "../../game/solitaire.js";
// --- Game State & Database Imports --- // --- Game State & Database Imports ---
import { activeSolitaireGames } from '../../game/state.js'; import { activeSolitaireGames } from "../../game/state.js";
import { import {
getSOTD, getUser, insertSOTDStats, deleteUserSOTDStats, getSOTD,
getUserSOTDStats, updateUserCoins, insertLog, getAllSOTDStats getUser,
} from '../../database/index.js'; insertSOTDStats,
import {socketEmit} from "../socket.js"; deleteUserSOTDStats,
getUserSOTDStats,
updateUserCoins,
insertLog,
getAllSOTDStats,
} from "../../database/index.js";
import { socketEmit } from "../socket.js";
// Create a new router instance // Create a new router instance
const router = express.Router(); const router = express.Router();
@@ -24,16 +41,18 @@ const router = express.Router();
* @returns {object} The configured Express router. * @returns {object} The configured Express router.
*/ */
export function solitaireRoutes(client, io) { export function solitaireRoutes(client, io) {
// --- Game Initialization Endpoints --- // --- Game Initialization Endpoints ---
router.post('/start', (req, res) => { router.post("/start", (req, res) => {
const { userId, userSeed, hardMode } = req.body; 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 a game already exists for the user, return it instead of creating a new one.
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) { 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; let deck, seed;
@@ -47,7 +66,7 @@ export function solitaireRoutes(client, io) {
let numericSeed = 0; let numericSeed = 0;
for (let i = 0; i < seed.length; i++) { for (let i = 0; i < seed.length; i++) {
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xFFFFFFFF; numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xffffffff;
} }
const rng = createSeededRNG(numericSeed); const rng = createSeededRNG(numericSeed);
@@ -66,19 +85,22 @@ export function solitaireRoutes(client, io) {
res.json({ success: true, gameState }); res.json({ success: true, gameState });
}); });
router.post('/start/sotd', (req, res) => { router.post("/start/sotd", (req, res) => {
const { userId } = req.body; const { userId } = req.body;
/*if (!userId || !getUser.get(userId)) { /*if (!userId || !getUser.get(userId)) {
return res.status(404).json({ error: 'User not found.' }); return res.status(404).json({ error: 'User not found.' });
}*/ }*/
if (activeSolitaireGames[userId]?.isSOTD) { if (activeSolitaireGames[userId]?.isSOTD) {
return res.json({ success: true, gameState: activeSolitaireGames[userId] }); return res.json({
success: true,
gameState: activeSolitaireGames[userId],
});
} }
const sotd = getSOTD.get(); const sotd = getSOTD.get();
if (!sotd) { 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 = { const gameState = {
@@ -104,49 +126,49 @@ export function solitaireRoutes(client, io) {
// --- Game State & Action Endpoints --- // --- Game State & Action Endpoints ---
router.get('/sotd/rankings', (req, res) => { router.get("/sotd/rankings", (req, res) => {
try { try {
const rankings = getAllSOTDStats.all(); const rankings = getAllSOTDStats.all();
res.json({ rankings }); res.json({ rankings });
} catch(e) { } catch (e) {
res.status(500).json({ error: "Failed to fetch SOTD rankings."}); res.status(500).json({ error: "Failed to fetch SOTD rankings." });
} }
}); });
router.get('/state/:userId', (req, res) => { router.get("/state/:userId", (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const gameState = activeSolitaireGames[userId]; const gameState = activeSolitaireGames[userId];
if (gameState) { if (gameState) {
res.json({ success: true, gameState }); res.json({ success: true, gameState });
} else { } 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; const { userId } = req.body;
if (activeSolitaireGames[userId]) { if (activeSolitaireGames[userId]) {
delete activeSolitaireGames[userId]; delete activeSolitaireGames[userId];
} }
res.json({ success: true, message: "Game reset."}); 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 { userId, ...moveData } = req.body;
const gameState = activeSolitaireGames[userId]; const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' }); 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.isDone) return res.status(400).json({ error: "This game is already completed." });
if (isValidMove(gameState, moveData)) { if (isValidMove(gameState, moveData)) {
moveCard(gameState, moveData); moveCard(gameState, moveData);
updateGameStats(gameState, 'move', moveData); updateGameStats(gameState, "move", moveData);
if (!gameState.autocompleting) { if (!gameState.autocompleting) {
const canAutoSolve = checkAutoSolve(gameState); const canAutoSolve = checkAutoSolve(gameState);
if (canAutoSolve) { if (canAutoSolve) {
gameState.autocompleting = true; gameState.autocompleting = true;
autoSolveMoves(userId, gameState) autoSolveMoves(userId, gameState);
} }
} }
@@ -157,42 +179,41 @@ export function solitaireRoutes(client, io) {
} }
res.json({ success: true, gameState, win }); res.json({ success: true, gameState, win });
} else { } 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 { userId } = req.body;
const gameState = activeSolitaireGames[userId]; const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' }); 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.isDone) return res.status(400).json({ error: "This game is already completed." });
if (gameState.hardMode) { if (gameState.hardMode) {
draw3Cards(gameState); draw3Cards(gameState);
} else { } else {
drawCard(gameState); drawCard(gameState);
} }
updateGameStats(gameState, 'draw'); updateGameStats(gameState, "draw");
res.json({ success: true, gameState }); res.json({ success: true, gameState });
}); });
router.post('/undo', (req, res) => { router.post("/undo", (req, res) => {
const { userId } = req.body; const { userId } = req.body;
const gameState = activeSolitaireGames[userId]; const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' }); 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.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.hist.length === 0) return res.status(400).json({ error: "No moves to undo." });
undoMove(gameState); undoMove(gameState);
res.json({ success: true, gameState }); res.json({ success: true, gameState });
}) });
return router; return router;
} }
// --- Helper Functions --- // --- Helper Functions ---
/** Updates game stats like moves and score after an action. */ /** 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 // if (!gameState.isSOTD) return; // Only track stats for SOTD
gameState.moves++; gameState.moves++;
if (actionType === 'move') { if (actionType === "move") {
if (moveData.destPileType === 'foundationPiles') { if (moveData.destPileType === "foundationPiles") {
gameState.score += 10; // Move card to foundation gameState.score += 10; // Move card to foundation
} }
if (moveData.sourcePileType === 'foundationPiles') { if (moveData.sourcePileType === "foundationPiles") {
gameState.score -= 15; // Move card from foundation (penalty) 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 // Penalty for cycling through an empty stock pile
gameState.score -= 5; gameState.score -= 5;
} }
@@ -224,11 +245,14 @@ async function handleWin(userId, gameState, io) {
const newCoins = currentUser.coins + bonus; const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins }); updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({ insertLog.run({
id: `${userId}-hardmode-solitaire-${Date.now()}`, user_id: userId, id: `${userId}-hardmode-solitaire-${Date.now()}`,
action: 'HARDMODE_SOLITAIRE_WIN', target_user_id: null, user_id: userId,
coins_amount: bonus, user_new_amount: newCoins, 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 if (!gameState.isSOTD) return; // Only process SOTD wins here
@@ -244,28 +268,35 @@ async function handleWin(userId, gameState, io) {
const newCoins = currentUser.coins + bonus; const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins }); updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({ insertLog.run({
id: `${userId}-sotd-complete-${Date.now()}`, user_id: userId, id: `${userId}-sotd-complete-${Date.now()}`,
action: 'SOTD_WIN', target_user_id: null, user_id: userId,
coins_amount: bonus, user_new_amount: newCoins, 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 // 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.score === existingStats.score && gameState.moves < existingStats.moves) || (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) { if (isNewBest) {
deleteUserSOTDStats.run(userId) deleteUserSOTDStats.run(userId);
insertSOTDStats.run({ insertSOTDStats.run({
id: userId, user_id: userId, id: userId,
user_id: userId,
time: timeTaken, time: timeTaken,
moves: gameState.moves, moves: gameState.moves,
score: gameState.score, score: gameState.score,
}); });
await socketEmit('sotd-update') await socketEmit("sotd-update");
console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`); 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 { import {
activeTicTacToeGames, activeTicTacToeGames,
tictactoeQueue, tictactoeQueue,
activeConnect4Games, activeConnect4Games,
connect4Queue, connect4Queue,
queueMessagesEndpoints, activePredis queueMessagesEndpoints,
} from '../game/state.js'; activePredis,
import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js'; } from "../game/state.js";
import { eloHandler } from '../game/elo.js'; import {
createConnect4Board,
formatConnect4BoardForDiscord,
checkConnect4Win,
checkConnect4Draw,
C4_ROWS,
} from "../game/various.js";
import { eloHandler } from "../game/elo.js";
import { getUser } from "../database/index.js"; import { getUser } from "../database/index.js";
// --- Module-level State --- // --- Module-level State ---
@@ -18,8 +25,8 @@ let io;
export function initializeSocket(server, client) { export function initializeSocket(server, client) {
io = server; io = server;
io.on('connection', (socket) => { io.on("connection", (socket) => {
socket.on('user-connected', async (userId) => { socket.on("user-connected", async (userId) => {
if (!userId) return; if (!userId) return;
await refreshQueuesForUser(userId, client); await refreshQueuesForUser(userId, client);
}); });
@@ -27,15 +34,15 @@ export function initializeSocket(server, client) {
registerTicTacToeEvents(socket, client); registerTicTacToeEvents(socket, client);
registerConnect4Events(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 // catch tab kills / network drops
socket.on('disconnecting', async () => { socket.on("disconnecting", async () => {
const discordId = socket.handshake.auth?.discordId; // or your mapping const discordId = socket.handshake.auth?.discordId; // or your mapping
await refreshQueuesForUser(discordId, client); await refreshQueuesForUser(discordId, client);
}); });
socket.on('disconnect', () => { socket.on("disconnect", () => {
// //
}); });
}); });
@@ -50,17 +57,17 @@ export function getSocketIo() {
// --- Event Registration --- // --- Event Registration ---
function registerTicTacToeEvents(socket, client) { function registerTicTacToeEvents(socket, client) {
socket.on('tictactoeconnection', (e) => refreshQueuesForUser(e.id, client)); socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client));
socket.on('tictactoequeue', (e) => onQueueJoin(client, 'tictactoe', e.playerId)); socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId));
socket.on('tictactoeplaying', (e) => onTicTacToeMove(client, e)); socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e));
socket.on('tictactoegameOver', (e) => onGameOver(client, 'tictactoe', e.playerId, e.winner)); socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner));
} }
function registerConnect4Events(socket, client) { function registerConnect4Events(socket, client) {
socket.on('connect4connection', (e) => refreshQueuesForUser(e.id, client)); socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client));
socket.on('connect4queue', (e) => onQueueJoin(client, 'connect4', e.playerId)); socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId));
socket.on('connect4playing', (e) => onConnect4Move(client, e)); socket.on("connect4playing", (e) => onConnect4Move(client, e));
socket.on('connect4NoTime', (e) => onGameOver(client, 'connect4', e.playerId, e.winner, '(temps écoulé)')); socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
} }
// --- Core Handlers (Preserving Original Logic) --- // --- Core Handlers (Preserving Original Logic) ---
@@ -69,7 +76,10 @@ async function onQueueJoin(client, gameType, playerId) {
if (!playerId) return; if (!playerId) return;
const { queue, activeGames, title, url } = getGameAssets(gameType); 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; return;
} }
@@ -89,12 +99,17 @@ async function onQueueJoin(client, gameType, playerId) {
*/ */
function checkTicTacToeWin(moves) { function checkTicTacToeWin(moves) {
const winningCombinations = [ const winningCombinations = [
[1, 2, 3], [4, 5, 6], [7, 8, 9], // Rows [1, 2, 3],
[1, 4, 7], [2, 5, 8], [3, 6, 9], // Columns [4, 5, 6],
[1, 5, 9], [3, 5, 7] // Diagonals [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) { for (const combination of winningCombinations) {
if (combination.every(num => moves.includes(num))) { if (combination.every((num) => moves.includes(num))) {
return true; return true;
} }
} }
@@ -103,11 +118,13 @@ function checkTicTacToeWin(moves) {
async function onTicTacToeMove(client, eventData) { async function onTicTacToeMove(client, eventData) {
const { playerId, value, boxId } = 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; if (!lobby) return;
const isP1Turn = lobby.sum % 2 === 1 && value === 'X' && lobby.p1.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; const isP2Turn = lobby.sum % 2 === 0 && value === "O" && lobby.p2.id === playerId;
if (isP1Turn || isP2Turn) { if (isP1Turn || isP2Turn) {
const playerMoves = isP1Turn ? lobby.xs : lobby.os; const playerMoves = isP1Turn ? lobby.xs : lobby.os;
@@ -115,28 +132,32 @@ async function onTicTacToeMove(client, eventData) {
lobby.sum++; lobby.sum++;
lobby.lastmove = Date.now(); lobby.lastmove = Date.now();
if (isP1Turn) lobby.p1.move = boxId if (isP1Turn) lobby.p1.move = boxId;
if (isP2Turn) lobby.p2.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); const hasWon = checkTicTacToeWin(playerMoves);
if (hasWon) { if (hasWon) {
// The current player has won. End the game. // 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) { } else if (lobby.sum > 9) {
// It's a draw (9 moves made, sum is now 10). End the game. // 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 { } else {
// The game continues. Update the state and notify clients. // 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) { async function onConnect4Move(client, eventData) {
const { playerId, col } = 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; if (!lobby || lobby.turn !== playerId) return;
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2; const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
@@ -160,17 +181,19 @@ async function onConnect4Move(client, eventData) {
winnerId = null; // Represents a draw winnerId = null; // Represents a draw
} else { } else {
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id; lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
io.emit('connect4playing', { allPlayers: Object.values(activeConnect4Games) }); io.emit("connect4playing", {
await emitQueueUpdate(client, 'connact4'); allPlayers: Object.values(activeConnect4Games),
await updateDiscordMessage(client, lobby, 'Puissance 4'); });
await emitQueueUpdate(client, "connact4");
await updateDiscordMessage(client, lobby, "Puissance 4");
return; 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 { 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; const game = gameKey ? activeGames[gameKey] : undefined;
if (!game || game.gameOver) return; if (!game || game.gameOver) return;
@@ -178,20 +201,26 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = '') {
let resultText; let resultText;
if (winnerId === null) { if (winnerId === null) {
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase()); await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
resultText = 'Égalité'; resultText = "Égalité";
} else { } 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; const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
resultText = `Victoire de ${winnerName}`; resultText = `Victoire de ${winnerName}`;
} }
await updateDiscordMessage(client, game, title, `${resultText} ${reason}`); await updateDiscordMessage(client, game, title, `${resultText} ${reason}`);
if(gameType === 'tictactoe') io.emit('tictactoegameOver', { game, winner: winnerId }); if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
if(gameType === 'connect4') io.emit('connect4gameOver', { game, winner: winnerId }); if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
if (gameKey) { 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)]); const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
let lobby; let lobby;
if (gameType === 'tictactoe') { 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() }; lobby = {
} else { // connect4 p1: {
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: [] }; 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); const msgId = await updateDiscordMessage(client, lobby, title);
@@ -230,12 +296,16 @@ async function refreshQueuesForUser(userId, client) {
try { try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[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 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: [] }); await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId]; delete queueMessagesEndpoints[userId];
} catch (e) { } 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 { try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[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 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: [] }); await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId]; delete queueMessagesEndpoints[userId];
} catch (e) { } catch (e) {
console.error('Error updating queue message : ', e); console.error("Error updating queue message : ", e);
} }
} }
await emitQueueUpdate(client, 'tictactoe'); await emitQueueUpdate(client, "tictactoe");
await emitQueueUpdate(client, 'connect4'); await emitQueueUpdate(client, "connect4");
} }
async function emitQueueUpdate(client, gameType) { async function emitQueueUpdate(client, gameType) {
const { queue, activeGames } = getGameAssets(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); const user = await client.users.fetch(id).catch(() => null);
return user?.globalName || user?.username; 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) { function getGameAssets(gameType) {
if (gameType === 'tictactoe') return { queue: tictactoeQueue, activeGames: activeTicTacToeGames, title: 'Tic Tac Toe', url: '/tic-tac-toe' }; if (gameType === "tictactoe")
if (gameType === 'connect4') return { queue: connect4Queue, activeGames: activeConnect4Games, title: 'Puissance 4', url: '/connect-4' }; 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: {} }; return { queue: [], activeGames: {} };
} }
@@ -277,28 +368,48 @@ async function postQueueToDiscord(client, playerId, title, url) {
try { try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(playerId); 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 embed = new EmbedBuilder()
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)); .setTitle(title)
const msg = await generalChannel.send({ embeds: [embed], components: [row] }); .setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`)
queueMessagesEndpoints[playerId] = msg.id .setColor("#5865F2")
} catch (e) { console.error(`Failed to post queue message for ${title}:`, e); } .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); const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
if (!channel) return null; if (!channel) return null;
let description; let description;
if (title === 'Tic Tac Toe') { if (title === "Tic Tac Toe") {
let gridText = ''; let gridText = "";
for (let i = 1; i <= 9; i++) { gridText += game.xs.includes(i) ? '❌' : game.os.includes(i) ? '⭕' : '🟦'; if (i % 3 === 0) gridText += '\n'; } 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}`; description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
} else { } else {
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`; description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
} }
if (resultText) description += `\n### ${resultText}`; 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 { try {
if (game.msgId) { if (game.msgId) {
@@ -309,22 +420,24 @@ async function updateDiscordMessage(client, game, title, resultText = '') {
const message = await channel.send({ embeds: [embed] }); const message = await channel.send({ embeds: [embed] });
return message.id; return message.id;
} }
} catch (e) { return null; } } catch (e) {
return null;
}
} }
function cleanupStaleGames() { function cleanupStaleGames() {
const now = Date.now(); const now = Date.now();
const STALE_TIMEOUT = 30 * 60 * 1000; const STALE_TIMEOUT = 30 * 60 * 1000;
const cleanup = (games, name) => { const cleanup = (games, name) => {
Object.keys(games).forEach(key => { Object.keys(games).forEach((key) => {
if (now - games[key].lastmove > STALE_TIMEOUT) { if (now - games[key].lastmove > STALE_TIMEOUT) {
console.log(`[Cleanup] Removing stale ${name} game: ${key}`); console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
delete games[key]; delete games[key];
} }
}); });
}; };
cleanup(activeTicTacToeGames, 'TicTacToe'); cleanup(activeTicTacToeGames, "TicTacToe");
cleanup(activeConnect4Games, 'Connect4'); cleanup(activeConnect4Games, "Connect4");
} }
/* EMITS */ /* EMITS */
@@ -332,18 +445,18 @@ export async function socketEmit(event, data) {
io.emit(event, data); io.emit(event, data);
} }
export async function emitDataUpdated(data) { export async function emitDataUpdated(data) {
io.emit('data-updated', data); io.emit("data-updated", data);
} }
export async function emitPokerUpdate(data) { export async function emitPokerUpdate(data) {
io.emit('poker-update', data); io.emit("poker-update", data);
} }
export async function emitPokerToast(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 emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
export const emitToast = (payload) => io.emit("blackjack:toast", payload); 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 OpenAI from "openai";
import {GoogleGenAI} from "@google/genai"; import { GoogleGenAI } from "@google/genai";
import {Mistral} from '@mistralai/mistralai'; import { Mistral } from "@mistralai/mistralai";
// --- AI Client Initialization --- // --- AI Client Initialization ---
// Initialize clients for each AI service. This is done once when the module is loaded. // Initialize clients for each AI service. This is done once when the module is loaded.
@@ -13,15 +13,14 @@ if (process.env.OPENAI_API_KEY) {
let gemini; let gemini;
if (process.env.GEMINI_KEY) { if (process.env.GEMINI_KEY) {
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY}) gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY });
} }
let mistral; let mistral;
if (process.env.MISTRAL_KEY) { if (process.env.MISTRAL_KEY) {
mistral = new Mistral({apiKey: process.env.MISTRAL_KEY}); mistral = new Mistral({ apiKey: process.env.MISTRAL_KEY });
} }
/** /**
* Gets a response from the configured AI model. * Gets a response from the configured AI model.
* It dynamically chooses the provider based on the MODEL environment variable. * It dynamically chooses the provider based on the MODEL environment variable.
@@ -35,7 +34,7 @@ export async function gork(messageHistory) {
try { try {
// --- OpenAI Provider --- // --- OpenAI Provider ---
if (modelProvider === 'OpenAI' && openai) { if (modelProvider === "OpenAI" && openai) {
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-5", // Using a modern, cost-effective model model: "gpt-5", // Using a modern, cost-effective model
reasoning_effort: "low", reasoning_effort: "low",
@@ -45,15 +44,15 @@ export async function gork(messageHistory) {
} }
// --- Google Gemini Provider --- // --- Google Gemini Provider ---
else if (modelProvider === 'Gemini' && gemini) { else if (modelProvider === "Gemini" && gemini) {
// Gemini requires a slightly different history format. // Gemini requires a slightly different history format.
const contents = messageHistory.map(msg => ({ const contents = messageHistory.map((msg) => ({
role: msg.role === 'assistant' ? 'model' : msg.role, // Gemini uses 'model' for assistant role role: msg.role === "assistant" ? "model" : msg.role, // Gemini uses 'model' for assistant role
parts: [{ text: msg.content }], parts: [{ text: msg.content }],
})); }));
// The last message should not be from the model // 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(); contents.pop();
} }
@@ -63,9 +62,9 @@ export async function gork(messageHistory) {
} }
// --- Mistral Provider --- // --- Mistral Provider ---
else if (modelProvider === 'Mistral' && mistral) { else if (modelProvider === "Mistral" && mistral) {
const chatResponse = await mistral.chat({ const chatResponse = await mistral.chat({
model: 'mistral-large-latest', model: "mistral-large-latest",
messages: messageHistory, messages: messageHistory,
}); });
return chatResponse.choices[0].message.content; return chatResponse.choices[0].message.content;
@@ -76,23 +75,22 @@ export async function gork(messageHistory) {
console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`); console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`);
return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur."; return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur.";
} }
} catch(error) { } catch (error) {
console.error(`[AI] Error with ${modelProvider} API:`, error); console.error(`[AI] Error with ${modelProvider} API:`, error);
return "Oups, une erreur est survenue en contactant le service d'IA."; return "Oups, une erreur est survenue en contactant le service d'IA.";
} }
} }
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || '100', 10); 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 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 INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || "true") === "true";
export const stripMentionsOfBot = (text, botId) => export const stripMentionsOfBot = (text, botId) => text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim();
export const sanitize = (s) => export const sanitize = (s) =>
(s || '') (s || "")
.replace(/\s+/g, ' ') .replace(/\s+/g, " ")
.replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels .replace(/```/g, "ʼʼʼ") // éviter de casser des fences éventuels
.trim(); .trim();
export const shortTs = (d) => new Date(d).toISOString(); // compact et triable 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; if (!content && atts.length === 0) continue;
const attMeta = atts.length 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, id: a.id,
name: a.name, name: a.name,
type: a.contentType || 'application/octet-stream', type: a.contentType || "application/octet-stream",
size: a.size, size: a.size,
isImage: !!(a.contentType && a.contentType.startsWith('image/')), isImage: !!(a.contentType && a.contentType.startsWith("image/")),
width: a.width || undefined, width: a.width || undefined,
height: a.height || 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 url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
})) }))
: undefined; : undefined;
@@ -140,19 +138,19 @@ export function buildTranscript(messages, botId) {
id: m.author.id, id: m.author.id,
nick: m.member?.nickname || m.author.globalName || m.author.username, nick: m.member?.nickname || m.author.globalName || m.author.username,
isBot: !!m.author.bot, isBot: !!m.author.bot,
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''), mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ""),
replyTo: m.reference?.messageId || null, replyTo: m.reference?.messageId || null,
content, content,
attachments: attMeta, attachments: attMeta,
}; };
lines.push(line); lines.push(line);
} }
return lines.map(l => JSON.stringify(l)).join('\n'); return lines.map((l) => JSON.stringify(l)).join("\n");
} }
export function buildAiMessages({ export function buildAiMessages({
botId, botId,
botName = 'FlopoBot', botName = "FlopoBot",
invokerId, invokerId,
invokerName, invokerName,
requestText, requestText,
@@ -162,9 +160,8 @@ export function buildAiMessages({
invokerAttachments = [], invokerAttachments = [],
}) { }) {
const system = { const system = {
role: 'system', role: "system",
content: content: `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
`Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
Règles de sortie: Règles de sortie:
- Réponds en français, en 13 phrases. - Réponds en français, en 13 phrases.
- Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif. - Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif.
@@ -174,24 +171,31 @@ export function buildAiMessages({
}; };
const attLines = invokerAttachments.length 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 = { const user = {
role: 'user', role: "user",
content: content: `Tâche: répondre brièvement à <@${invokerId}>.
`Tâche: répondre brièvement à <@${invokerId}>.
Message de <@${invokerId}> (${invokerName || 'inconnu'}): Message de <@${invokerId}> (${invokerName || "inconnu"}):
""" """
${requestText} ${requestText}
""" """
${invokerAttachments.length ? `Pièces jointes du message: ${
invokerAttachments.length
? `Pièces jointes du message:
${attLines} ${attLines}
` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''} `
: ""
}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ""}
Participants (id -> nom): 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): Contexte (transcript JSONL; à utiliser seulement si utile):
\`\`\`jsonl \`\`\`jsonl

View File

@@ -1,75 +1,75 @@
export const roles = { export const roles = {
erynie_1: { erynie_1: {
name: 'Erinye', name: "Erinye",
subtitle: 'Mégère, la haine', subtitle: "Mégère, la haine",
descr: '', descr: "",
powers: { powers: {
double_vote: { 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, charges: 1,
disabled: false, disabled: false,
}, },
}, },
passive: {}, passive: {},
team: 'Erinyes', team: "Erinyes",
}, },
erynie_2: { erynie_2: {
name: 'Erinye', name: "Erinye",
subtitle: 'Tisiphone, la vengeance', subtitle: "Tisiphone, la vengeance",
descr: '', descr: "",
powers: { powers: {
one_shot: { 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, charges: 1,
disabled: false, disabled: false,
}, },
}, },
passive: {}, passive: {},
team: 'Erinyes', team: "Erinyes",
}, },
erynie_3: { erynie_3: {
name: 'Erinye', name: "Erinye",
subtitle: 'Alecto, l\'implacable', subtitle: "Alecto, l'implacable",
descr: '', descr: "",
powers: { powers: {
silence: { 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, charges: 999,
disabled: false, disabled: false,
} },
}, },
passive: { passive: {
descr: 'Voit quels pouvoirs ont été utilisés.', descr: "Voit quels pouvoirs ont été utilisés.",
disabled: false, disabled: false,
}, },
team: 'Erinyes', team: "Erinyes",
}, },
narcisse: { narcisse: {
name: 'Narcisse', name: "Narcisse",
subtitle: '', subtitle: "",
descr: '', descr: "",
powers: {}, powers: {},
passive: { passive: {
descr: 'S\'il devient maire ...', descr: "S'il devient maire ...",
disabled: false, disabled: false,
}, },
}, },
charon: { charon: {
name: 'Charon', name: "Charon",
subtitle: 'Sorcier', subtitle: "Sorcier",
descr: 'C\'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.', descr: "C'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.",
powers: { powers: {
revive: { revive: {
descr: 'Refuser de faire traverser le Styx (sauver quelqu\'un)', descr: "Refuser de faire traverser le Styx (sauver quelqu'un)",
charges: 1, charges: 1,
disabled: false, disabled: false,
}, },
kill: { kill: {
descr: 'Traverser le Styx (tuer quelqu\'un)', descr: "Traverser le Styx (tuer quelqu'un)",
charges: 1, charges: 1,
disabled: false, disabled: false,
} },
}, },
}, },
//... //...
} };

View File

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