mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-01-18 16:37:40 +01:00
fix: big fix + prettier
This commit is contained in:
61
.idea/codeStyles/Project.xml
generated
Normal file
61
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
2
.idea/inspectionProfiles/Project_Default.xml
generated
2
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="EditorConfigDeprecatedDescriptor" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal 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
7
.idea/prettier.xml
generated
Normal 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
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"useTabs": true
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
# FLOPOBOT DEUXIEME DU NOM
|
||||
|
||||
## Project structure
|
||||
|
||||
Below is a basic overview of the project structure:
|
||||
|
||||
```
|
||||
@@ -15,5 +16,6 @@ Below is a basic overview of the project structure:
|
||||
```
|
||||
|
||||
## FlopoSite
|
||||
|
||||
- FlopoBot has its own website to use it a different way
|
||||
[FlopoSite's repo](https://github.com/cassoule/floposite)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { registerCommands } from './src/config/commands.js';
|
||||
import { registerCommands } from "./src/config/commands.js";
|
||||
|
||||
console.log('Registering global commands...');
|
||||
console.log("Registering global commands...");
|
||||
registerCommands();
|
||||
console.log('Commands registered.');
|
||||
console.log("Commands registered.");
|
||||
|
||||
9
eslint.config.js
Normal file
9
eslint.config.js
Normal 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" },
|
||||
]);
|
||||
54
index.js
54
index.js
@@ -1,53 +1,51 @@
|
||||
import 'dotenv/config';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import "dotenv/config";
|
||||
import http from "http";
|
||||
import { Server } from "socket.io";
|
||||
|
||||
import { app } from './src/server/app.js';
|
||||
import { client } from './src/bot/client.js';
|
||||
import { initializeEvents } from './src/bot/events.js';
|
||||
import { initializeSocket } from './src/server/socket.js';
|
||||
import { getAkhys, setupCronJobs } from './src/utils/index.js';
|
||||
import { app } from "./src/server/app.js";
|
||||
import { client } from "./src/bot/client.js";
|
||||
import { initializeEvents } from "./src/bot/events.js";
|
||||
import { initializeSocket } from "./src/server/socket.js";
|
||||
import { setupCronJobs } from "./src/utils/index.js";
|
||||
|
||||
// --- SERVER INITIALIZATION ---
|
||||
const PORT = process.env.PORT || 25578;
|
||||
const server = http.createServer(app);
|
||||
|
||||
// --- SOCKET.IO INITIALIZATION ---
|
||||
const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||
export const io = new Server(server, {
|
||||
cors: {
|
||||
origin: FLAPI_URL,
|
||||
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
|
||||
},
|
||||
pingInterval: 5000,
|
||||
pingTimeout: 5000,
|
||||
cors: {
|
||||
origin: FLAPI_URL,
|
||||
methods: ["GET", "POST", "PUT", "OPTIONS"],
|
||||
},
|
||||
pingInterval: 5000,
|
||||
pingTimeout: 5000,
|
||||
});
|
||||
initializeSocket(io, client);
|
||||
|
||||
|
||||
// --- BOT INITIALIZATION ---
|
||||
initializeEvents(client, io);
|
||||
client.login(process.env.BOT_TOKEN).then(() => {
|
||||
console.log(`Logged in as ${client.user.tag}`);
|
||||
console.log('[Discord Bot Events Initialized]');
|
||||
console.log(`Logged in as ${client.user.tag}`);
|
||||
console.log("[Discord Bot Events Initialized]");
|
||||
});
|
||||
|
||||
|
||||
// --- APP STARTUP ---
|
||||
server.listen(PORT, async () => {
|
||||
console.log(`Express+Socket.IO server listening on port ${PORT}`);
|
||||
console.log(`[Connected with ${FLAPI_URL}]`);
|
||||
console.log(`Express+Socket.IO server listening on port ${PORT}`);
|
||||
console.log(`[Connected with ${FLAPI_URL}]`);
|
||||
|
||||
// Initial data fetch and setup
|
||||
try {
|
||||
// Initial data fetch and setup
|
||||
/*try {
|
||||
await getAkhys(client);
|
||||
} catch (error) {
|
||||
console.log('Initial Fetch Error');
|
||||
}
|
||||
}*/
|
||||
|
||||
// Setup scheduled tasks
|
||||
//setupCronJobs(client, io);
|
||||
console.log('[Cron Jobs Initialized]');
|
||||
// Setup scheduled tasks
|
||||
setupCronJobs(client, io);
|
||||
console.log("[Cron Jobs Initialized]");
|
||||
|
||||
console.log('--- FlopoBOT is ready ---');
|
||||
console.log("--- FlopoBOT is ready ---");
|
||||
});
|
||||
6696
package-lock.json
generated
6696
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@@ -1,37 +1,41 @@
|
||||
{
|
||||
"name": "t12_flopobot",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Flopobot le 2 donc en mieux",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"register": "node commands.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"author": "Milo Gourvest",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@google/genai": "^0.8.0",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"axios": "^1.9.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"discord-interactions": "^4.0.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.104.0",
|
||||
"pokersolver": "^2.1.4",
|
||||
"socket.io": "^4.8.1",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.0"
|
||||
}
|
||||
"name": "t12_flopobot",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Flopobot le 2 donc en mieux",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"register": "node commands.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"author": "Milo Gourvest",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@google/genai": "^0.8.0",
|
||||
"@mistralai/mistralai": "^1.6.0",
|
||||
"axios": "^1.9.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"discord-interactions": "^4.0.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.104.0",
|
||||
"pokersolver": "^2.1.4",
|
||||
"socket.io": "^4.8.1",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/json": "^0.14.0",
|
||||
"eslint": "^9.39.1",
|
||||
"globals": "^16.5.0",
|
||||
"nodemon": "^3.0.0",
|
||||
"prettier": "3.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":disableDependencyDashboard",
|
||||
":preserveSemverRanges"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", ":disableDependencyDashboard", ":preserveSemverRanges"],
|
||||
"ignorePaths": ["**/node_modules/**"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import "dotenv/config";
|
||||
|
||||
/**
|
||||
* A generic function for making requests to the Discord API.
|
||||
@@ -10,38 +10,38 @@ import 'dotenv/config';
|
||||
* @throws Will throw an error if the API request is not successful.
|
||||
*/
|
||||
export async function DiscordRequest(endpoint, options) {
|
||||
// Construct the full API URL
|
||||
const url = 'https://discord.com/api/v10/' + endpoint;
|
||||
// Construct the full API URL
|
||||
const url = "https://discord.com/api/v10/" + endpoint;
|
||||
|
||||
// Stringify the payload if it exists
|
||||
if (options && options.body) {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
// Stringify the payload if it exists
|
||||
if (options && options.body) {
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
// Use fetch to make the request, automatically including required headers
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`,
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)',
|
||||
},
|
||||
...options, // Spread the given options (e.g., method, body)
|
||||
});
|
||||
// Use fetch to make the request, automatically including required headers
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"User-Agent": "DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)",
|
||||
},
|
||||
...options, // Spread the given options (e.g., method, body)
|
||||
});
|
||||
|
||||
// If the request was not successful, throw a detailed error
|
||||
if (!res.ok) {
|
||||
let data
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (err) {
|
||||
data = res;
|
||||
}
|
||||
console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data);
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
// If the request was not successful, throw a detailed error
|
||||
if (!res.ok) {
|
||||
let data;
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (err) {
|
||||
data = res;
|
||||
}
|
||||
console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data);
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Return the original response object for further processing
|
||||
return res;
|
||||
// Return the original response object for further processing
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,15 +51,15 @@ export async function DiscordRequest(endpoint, options) {
|
||||
* @param {Array<object>} commands - An array of command objects to install.
|
||||
*/
|
||||
export async function InstallGlobalCommands(appId, commands) {
|
||||
// API endpoint for bulk overwriting global commands
|
||||
const endpoint = `applications/${appId}/commands`;
|
||||
// API endpoint for bulk overwriting global commands
|
||||
const endpoint = `applications/${appId}/commands`;
|
||||
|
||||
console.log('Installing global commands...');
|
||||
try {
|
||||
// This uses the generic DiscordRequest function to make the API call
|
||||
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
|
||||
console.log('Successfully installed global commands.');
|
||||
} catch (err) {
|
||||
console.error('Error installing global commands:', err);
|
||||
}
|
||||
console.log("Installing global commands...");
|
||||
try {
|
||||
// This uses the generic DiscordRequest function to make the API call
|
||||
await DiscordRequest(endpoint, { method: "PUT", body: commands });
|
||||
console.log("Successfully installed global commands.");
|
||||
} catch (err) {
|
||||
console.error("Error installing global commands:", err);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
export async function getValorantSkins(locale='fr-FR') {
|
||||
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' });
|
||||
const data = await response.json();
|
||||
return data.data
|
||||
export async function getValorantSkins(locale = "fr-FR") {
|
||||
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: "GET" });
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function getSkinTiers(locale='fr-FR') {
|
||||
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'});
|
||||
const data = await response.json();
|
||||
return data.data
|
||||
export async function getSkinTiers(locale = "fr-FR") {
|
||||
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: "GET" });
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { Client, GatewayIntentBits } from "discord.js";
|
||||
|
||||
/**
|
||||
* The single, shared Discord.js Client instance for the entire application.
|
||||
* It is configured with all the necessary intents to receive the events it needs.
|
||||
*/
|
||||
export const client = new Client({
|
||||
// Define the events the bot needs to receive from Discord's gateway.
|
||||
intents: [
|
||||
// Required for basic guild information and events.
|
||||
GatewayIntentBits.Guilds,
|
||||
// Define the events the bot needs to receive from Discord's gateway.
|
||||
intents: [
|
||||
// Required for basic guild information and events.
|
||||
GatewayIntentBits.Guilds,
|
||||
|
||||
// Required to receive messages in guilds (e.g., in #general).
|
||||
GatewayIntentBits.GuildMessages,
|
||||
// Required to receive messages in guilds (e.g., in #general).
|
||||
GatewayIntentBits.GuildMessages,
|
||||
|
||||
// A PRIVILEGED INTENT, required to read the content of messages.
|
||||
// This is necessary for the AI handler, admin commands, and "quoi/feur".
|
||||
GatewayIntentBits.MessageContent,
|
||||
// A PRIVILEGED INTENT, required to read the content of messages.
|
||||
// This is necessary for the AI handler, admin commands, and "quoi/feur".
|
||||
GatewayIntentBits.MessageContent,
|
||||
|
||||
// Required to receive updates when members join, leave, or are updated.
|
||||
// Crucial for fetching member details for commands like /timeout or /info.
|
||||
GatewayIntentBits.GuildMembers,
|
||||
// Required to receive updates when members join, leave, or are updated.
|
||||
// Crucial for fetching member details for commands like /timeout or /info.
|
||||
GatewayIntentBits.GuildMembers,
|
||||
|
||||
// Required to receive member presence updates (online, idle, offline).
|
||||
// Necessary for features like `getOnlineUsersWithRole`.
|
||||
GatewayIntentBits.GuildPresences,
|
||||
],
|
||||
// Required to receive member presence updates (online, idle, offline).
|
||||
// Necessary for features like `getOnlineUsersWithRole`.
|
||||
GatewayIntentBits.GuildPresences,
|
||||
],
|
||||
});
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
import { InteractionResponseType, MessageComponentTypes, ButtonStyleTypes } from "discord-interactions";
|
||||
|
||||
/**
|
||||
* Handles the /floposite slash command.
|
||||
@@ -11,45 +7,45 @@ import {
|
||||
* @param {object} res - The Express response object.
|
||||
*/
|
||||
export async function handleFlopoSiteCommand(req, res) {
|
||||
// The URL for the link button. Consider moving to .env if it changes.
|
||||
const siteUrl = process.env.FLOPOSITE_URL || 'https://floposite.com';
|
||||
// The URL for the link button. Consider moving to .env if it changes.
|
||||
const siteUrl = process.env.FLOPOSITE_URL || "https://floposite.com";
|
||||
|
||||
// The URL for the thumbnail image.
|
||||
const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`;
|
||||
// The URL for the thumbnail image.
|
||||
const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`;
|
||||
|
||||
// Define the components (the link button)
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
label: 'Aller sur FlopoSite',
|
||||
style: ButtonStyleTypes.LINK,
|
||||
url: siteUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// Define the components (the link button)
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
label: "Aller sur FlopoSite",
|
||||
style: ButtonStyleTypes.LINK,
|
||||
url: siteUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Define the embed message
|
||||
const embeds = [
|
||||
{
|
||||
title: 'FlopoSite',
|
||||
description: "L'officiel et très goatesque site de FlopoBot.",
|
||||
color: 0x6571F3, // A custom blue color
|
||||
thumbnail: {
|
||||
url: thumbnailUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
// Define the embed message
|
||||
const embeds = [
|
||||
{
|
||||
title: "FlopoSite",
|
||||
description: "L'officiel et très goatesque site de FlopoBot.",
|
||||
color: 0x6571f3, // A custom blue color
|
||||
thumbnail: {
|
||||
url: thumbnailUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Send the response to Discord
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: embeds,
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
// Send the response to Discord
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: embeds,
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InteractionResponseType } from 'discord-interactions';
|
||||
import { InteractionResponseType } from "discord-interactions";
|
||||
|
||||
/**
|
||||
* Handles the /info slash command.
|
||||
@@ -8,64 +8,61 @@ import { InteractionResponseType } from 'discord-interactions';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleInfoCommand(req, res, client) {
|
||||
const { guild_id } = req.body;
|
||||
const { guild_id } = req.body;
|
||||
|
||||
try {
|
||||
// Fetch the guild object from the client
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
try {
|
||||
// Fetch the guild object from the client
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
|
||||
// Fetch all members to ensure the cache is up to date
|
||||
await guild.members.fetch();
|
||||
// Fetch all members to ensure the cache is up to date
|
||||
await guild.members.fetch();
|
||||
|
||||
// Filter the cached members to find those who are timed out
|
||||
// A member is timed out if their `communicationDisabledUntil` property is a future date.
|
||||
const timedOutMembers = guild.members.cache.filter(
|
||||
(member) =>
|
||||
member.communicationDisabledUntilTimestamp &&
|
||||
member.communicationDisabledUntilTimestamp > Date.now()
|
||||
);
|
||||
// Filter the cached members to find those who are timed out
|
||||
// A member is timed out if their `communicationDisabledUntil` property is a future date.
|
||||
const timedOutMembers = guild.members.cache.filter(
|
||||
(member) => member.communicationDisabledUntilTimestamp && member.communicationDisabledUntilTimestamp > Date.now(),
|
||||
);
|
||||
|
||||
// --- Case 1: No members are timed out ---
|
||||
if (timedOutMembers.size === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: 'Membres Timeout',
|
||||
description: "Aucun membre n'est actuellement timeout.",
|
||||
color: 0x4F545C, // Discord's gray color
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- Case 1: No members are timed out ---
|
||||
if (timedOutMembers.size === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Membres Timeout",
|
||||
description: "Aucun membre n'est actuellement timeout.",
|
||||
color: 0x4f545c, // Discord's gray color
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Case 2: At least one member is timed out ---
|
||||
// Format the list of timed-out members for the embed
|
||||
const memberList = timedOutMembers
|
||||
.map((member) => {
|
||||
// toLocaleString provides a user-friendly date and time format
|
||||
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString('fr-FR');
|
||||
return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`;
|
||||
})
|
||||
.join('\n');
|
||||
// --- Case 2: At least one member is timed out ---
|
||||
// Format the list of timed-out members for the embed
|
||||
const memberList = timedOutMembers
|
||||
.map((member) => {
|
||||
// toLocaleString provides a user-friendly date and time format
|
||||
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString("fr-FR");
|
||||
return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: 'Membres Actuellement Timeout',
|
||||
description: memberList,
|
||||
color: 0xED4245, // Discord's red color
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling /info command:', error);
|
||||
return res.status(500).json({ error: 'Failed to fetch timeout information.' });
|
||||
}
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Membres Actuellement Timeout",
|
||||
description: memberList,
|
||||
color: 0xed4245, // Discord's red color
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error handling /info command:", error);
|
||||
return res.status(500).json({ error: "Failed to fetch timeout information." });
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
InteractionResponseFlags,
|
||||
} from 'discord-interactions';
|
||||
import { activeInventories, skins } from '../../game/state.js';
|
||||
import { getUserInventory } from '../../database/index.js';
|
||||
InteractionResponseType,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
InteractionResponseFlags,
|
||||
} from "discord-interactions";
|
||||
import { activeInventories, skins } from "../../game/state.js";
|
||||
import { getUserInventory } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /inventory slash command.
|
||||
@@ -17,122 +17,149 @@ import { getUserInventory } from '../../database/index.js';
|
||||
* @param {string} interactionId - The unique ID of the interaction.
|
||||
*/
|
||||
export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||
const { member, guild_id, token, data } = req.body;
|
||||
const commandUserId = member.user.id;
|
||||
// User can specify another member, otherwise it defaults to themself
|
||||
const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId;
|
||||
const { member, guild_id, token, data } = req.body;
|
||||
const commandUserId = member.user.id;
|
||||
// User can specify another member, otherwise it defaults to themself
|
||||
const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId;
|
||||
|
||||
try {
|
||||
// --- 1. Fetch Data ---
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const targetMember = await guild.members.fetch(targetUserId);
|
||||
const inventorySkins = getUserInventory.all({ user_id: targetUserId });
|
||||
try {
|
||||
// --- 1. Fetch Data ---
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const targetMember = await guild.members.fetch(targetUserId);
|
||||
const inventorySkins = getUserInventory.all({ user_id: targetUserId });
|
||||
|
||||
// --- 2. Handle Empty Inventory ---
|
||||
if (inventorySkins.length === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
description: "Cet inventaire est vide.",
|
||||
color: 0x4F545C, // Discord Gray
|
||||
}],
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- 2. Handle Empty Inventory ---
|
||||
if (inventorySkins.length === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
description: "Cet inventaire est vide.",
|
||||
color: 0x4f545c, // Discord Gray
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Store Interactive Session State ---
|
||||
// This state is crucial for the component handlers to know which inventory to update.
|
||||
activeInventories[interactionId] = {
|
||||
akhyId: targetUserId, // The inventory owner
|
||||
userId: commandUserId, // The user who ran the command
|
||||
page: 0,
|
||||
amount: inventorySkins.length,
|
||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||
timestamp: Date.now(),
|
||||
inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn
|
||||
};
|
||||
// --- 3. Store Interactive Session State ---
|
||||
// This state is crucial for the component handlers to know which inventory to update.
|
||||
activeInventories[interactionId] = {
|
||||
akhyId: targetUserId, // The inventory owner
|
||||
userId: commandUserId, // The user who ran the command
|
||||
page: 0,
|
||||
amount: inventorySkins.length,
|
||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||
timestamp: Date.now(),
|
||||
inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn
|
||||
};
|
||||
|
||||
// --- 4. Prepare Embed Content ---
|
||||
const currentSkin = inventorySkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
// --- 4. Prepare Embed Content ---
|
||||
const currentSkin = inventorySkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? '💠 ' : '◾ ';
|
||||
}
|
||||
return result || 'N/A';
|
||||
};
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||
}
|
||||
return result || "N/A";
|
||||
};
|
||||
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName.replace(/[\r\n]+/g, ' ').replace(skinInfo.displayName, '').trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return 'Base';
|
||||
};
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName
|
||||
.replace(/[\r\n]+/g, " ")
|
||||
.replace(skinInfo.displayName, "")
|
||||
.trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return "Base";
|
||||
};
|
||||
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
|
||||
// --- 5. Build Initial Components (Buttons) ---
|
||||
const components = [
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
||||
];
|
||||
// --- 5. Build Initial Components (Buttons) ---
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `prev_page_${interactionId}`,
|
||||
label: "⏮️ Préc.",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `next_page_${interactionId}`,
|
||||
label: "Suiv. ⏭️",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
];
|
||||
|
||||
const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Only show upgrade button if the skin is upgradable AND the command user owns the inventory
|
||||
if (isUpgradable && targetUserId === commandUserId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
const isUpgradable =
|
||||
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Only show upgrade button if the skin is upgradable AND the command user owns the inventory
|
||||
if (isUpgradable && targetUserId === commandUserId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 6. Send Final Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
||||
footer: { text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` },
|
||||
fields: [{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
}],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
}],
|
||||
components: [{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{ type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
|
||||
label: 'Voir sur FlopoSite',
|
||||
style: ButtonStyleTypes.LINK,}]
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling /inventory command:', error);
|
||||
return res.status(500).json({ error: 'Failed to generate inventory.' });
|
||||
}
|
||||
// --- 6. Send Final Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
|
||||
label: "Voir sur FlopoSite",
|
||||
style: ButtonStyleTypes.LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error handling /inventory command:", error);
|
||||
return res.status(500).json({ error: "Failed to generate inventory." });
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
import { activeSearchs, skins } from '../../game/state.js';
|
||||
import { getAllSkins } from '../../database/index.js';
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from "discord-interactions";
|
||||
import { activeSearchs, skins } from "../../game/state.js";
|
||||
import { getAllSkins } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /search slash command.
|
||||
@@ -17,106 +17,117 @@ import { getAllSkins } from '../../database/index.js';
|
||||
* @param {string} interactionId - The unique ID of the interaction.
|
||||
*/
|
||||
export async function handleSearchCommand(req, res, client, interactionId) {
|
||||
const { member, guild_id, token, data } = req.body;
|
||||
const userId = member.user.id;
|
||||
const searchValue = data.options[0].value.toLowerCase();
|
||||
const { member, guild_id, token, data } = req.body;
|
||||
const userId = member.user.id;
|
||||
const searchValue = data.options[0].value.toLowerCase();
|
||||
|
||||
try {
|
||||
// --- 1. Fetch and Filter Data ---
|
||||
const allDbSkins = getAllSkins.all();
|
||||
const resultSkins = allDbSkins.filter((skin) =>
|
||||
skin.displayName.toLowerCase().includes(searchValue) ||
|
||||
skin.tierText.toLowerCase().includes(searchValue)
|
||||
);
|
||||
try {
|
||||
// --- 1. Fetch and Filter Data ---
|
||||
const allDbSkins = getAllSkins.all();
|
||||
const resultSkins = allDbSkins.filter(
|
||||
(skin) =>
|
||||
skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue),
|
||||
);
|
||||
|
||||
// --- 2. Handle No Results ---
|
||||
if (resultSkins.length === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Aucun skin ne correspond à votre recherche.',
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- 2. Handle No Results ---
|
||||
if (resultSkins.length === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Aucun skin ne correspond à votre recherche.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Store Interactive Session State ---
|
||||
activeSearchs[interactionId] = {
|
||||
userId: userId,
|
||||
page: 0,
|
||||
amount: resultSkins.length,
|
||||
resultSkins: resultSkins,
|
||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||
timestamp: Date.now(),
|
||||
searchValue: searchValue,
|
||||
};
|
||||
// --- 3. Store Interactive Session State ---
|
||||
activeSearchs[interactionId] = {
|
||||
userId: userId,
|
||||
page: 0,
|
||||
amount: resultSkins.length,
|
||||
resultSkins: resultSkins,
|
||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||
timestamp: Date.now(),
|
||||
searchValue: searchValue,
|
||||
};
|
||||
|
||||
// --- 4. Prepare Initial Embed Content ---
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const currentSkin = resultSkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
// --- 4. Prepare Initial Embed Content ---
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const currentSkin = resultSkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
|
||||
// Fetch owner details if the skin is owned
|
||||
let ownerText = '';
|
||||
if (currentSkin.user_id) {
|
||||
try {
|
||||
const owner = await guild.members.fetch(currentSkin.user_id);
|
||||
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||
ownerText = '| Appartenant à un utilisateur inconnu';
|
||||
}
|
||||
}
|
||||
// Fetch owner details if the skin is owned
|
||||
let ownerText = "";
|
||||
if (currentSkin.user_id) {
|
||||
try {
|
||||
const owner = await guild.members.fetch(currentSkin.user_id);
|
||||
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||
ownerText = "| Appartenant à un utilisateur inconnu";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get the best possible image for the skin
|
||||
const getImageUrl = (skinInfo) => {
|
||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||
// Helper to get the best possible image for the skin
|
||||
const getImageUrl = (skinInfo) => {
|
||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||
|
||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
||||
|
||||
return skinInfo.displayIcon; // Fallback to base icon
|
||||
};
|
||||
return skinInfo.displayIcon; // Fallback to base icon
|
||||
};
|
||||
|
||||
// --- 5. Build Initial Components & Embed ---
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_search_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_search_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
||||
],
|
||||
},
|
||||
];
|
||||
// --- 5. Build Initial Components & Embed ---
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `prev_search_page_${interactionId}`,
|
||||
label: "⏮️ Préc.",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `next_search_page_${interactionId}`,
|
||||
label: "Suiv. ⏭️",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const embed = {
|
||||
title: 'Résultats de la recherche',
|
||||
description: `🔎 _"${searchValue}"_`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
||||
fields: [{
|
||||
name: `**${currentSkin.displayName}**`,
|
||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||
}],
|
||||
image: { url: getImageUrl(skinData) },
|
||||
footer: { text: `Résultat 1/${resultSkins.length}` },
|
||||
};
|
||||
const embed = {
|
||||
title: "Résultats de la recherche",
|
||||
description: `🔎 _"${searchValue}"_`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
fields: [
|
||||
{
|
||||
name: `**${currentSkin.displayName}**`,
|
||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(skinData) },
|
||||
footer: { text: `Résultat 1/${resultSkins.length}` },
|
||||
};
|
||||
|
||||
// --- 6. Send Final Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [embed],
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling /search command:', error);
|
||||
return res.status(500).json({ error: 'Failed to execute search.' });
|
||||
}
|
||||
// --- 6. Send Final Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [embed],
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error handling /search command:", error);
|
||||
return res.status(500).json({ error: "Failed to execute search." });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InteractionResponseType } from 'discord-interactions';
|
||||
import { getTopSkins } from '../../database/index.js';
|
||||
import { InteractionResponseType } from "discord-interactions";
|
||||
import { getTopSkins } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /skins slash command.
|
||||
@@ -9,60 +9,59 @@ import { getTopSkins } from '../../database/index.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleSkinsCommand(req, res, client) {
|
||||
const { guild_id } = req.body;
|
||||
const { guild_id } = req.body;
|
||||
|
||||
try {
|
||||
// --- 1. Fetch Data ---
|
||||
const topSkins = getTopSkins.all();
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const fields = [];
|
||||
try {
|
||||
// --- 1. Fetch Data ---
|
||||
const topSkins = getTopSkins.all();
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const fields = [];
|
||||
|
||||
// --- 2. Build Embed Fields Asynchronously ---
|
||||
// We use a for...of loop to handle the async fetch for each owner.
|
||||
for (const [index, skin] of topSkins.entries()) {
|
||||
let ownerText = 'Libre'; // Default text if the skin has no owner
|
||||
// --- 2. Build Embed Fields Asynchronously ---
|
||||
// We use a for...of loop to handle the async fetch for each owner.
|
||||
for (const [index, skin] of topSkins.entries()) {
|
||||
let ownerText = "Libre"; // Default text if the skin has no owner
|
||||
|
||||
// If the skin has an owner, fetch their details
|
||||
if (skin.user_id) {
|
||||
try {
|
||||
const owner = await guild.members.fetch(skin.user_id);
|
||||
// Use globalName if available, otherwise fallback to username
|
||||
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
|
||||
} catch (e) {
|
||||
// This can happen if the user has left the server
|
||||
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`);
|
||||
ownerText = 'Appartient à un utilisateur inconnu';
|
||||
}
|
||||
}
|
||||
// If the skin has an owner, fetch their details
|
||||
if (skin.user_id) {
|
||||
try {
|
||||
const owner = await guild.members.fetch(skin.user_id);
|
||||
// Use globalName if available, otherwise fallback to username
|
||||
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
|
||||
} catch (e) {
|
||||
// This can happen if the user has left the server
|
||||
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`);
|
||||
ownerText = "Appartient à un utilisateur inconnu";
|
||||
}
|
||||
}
|
||||
|
||||
// Add the formatted skin info to our fields array
|
||||
fields.push({
|
||||
name: `#${index + 1} - **${skin.displayName}**`,
|
||||
value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
// Add the formatted skin info to our fields array
|
||||
fields.push({
|
||||
name: `#${index + 1} - **${skin.displayName}**`,
|
||||
value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Send the Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: '🏆 Top 10 des Skins les Plus Chers',
|
||||
description: 'Classement des skins par leur valeur maximale potentielle.',
|
||||
fields: fields,
|
||||
color: 0xFFD700, // Gold color for a leaderboard
|
||||
footer: {
|
||||
text: 'Utilisez /inventory pour voir vos propres skins.'
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling /skins command:', error);
|
||||
return res.status(500).json({ error: 'Failed to fetch the skins leaderboard.' });
|
||||
}
|
||||
// --- 3. Send the Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: "🏆 Top 10 des Skins les Plus Chers",
|
||||
description: "Classement des skins par leur valeur maximale potentielle.",
|
||||
fields: fields,
|
||||
color: 0xffd700, // Gold color for a leaderboard
|
||||
footer: {
|
||||
text: "Utilisez /inventory pour voir vos propres skins.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error handling /skins command:", error);
|
||||
return res.status(500).json({ error: "Failed to fetch the skins leaderboard." });
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from "discord-interactions";
|
||||
|
||||
import { formatTime, getOnlineUsersWithRole } from '../../utils/index.js';
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import { activePolls } from '../../game/state.js';
|
||||
import { getSocketIo } from '../../server/socket.js';
|
||||
import { getUser } from '../../database/index.js';
|
||||
import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activePolls } from "../../game/state.js";
|
||||
import { getSocketIo } from "../../server/socket.js";
|
||||
import { getUser } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /timeout slash command.
|
||||
@@ -18,193 +18,225 @@ import { getUser } from '../../database/index.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleTimeoutCommand(req, res, client) {
|
||||
const io = getSocketIo();
|
||||
const { id, member, guild_id, channel_id, token, data } = req.body;
|
||||
const { options } = data;
|
||||
const io = getSocketIo();
|
||||
const { id, member, guild_id, channel_id, token, data } = req.body;
|
||||
const { options } = data;
|
||||
|
||||
// Extract command options
|
||||
const userId = member.user.id;
|
||||
const targetUserId = options[0].value;
|
||||
const time = options[1].value;
|
||||
// Extract command options
|
||||
const userId = member.user.id;
|
||||
const targetUserId = options[0].value;
|
||||
const time = options[1].value;
|
||||
|
||||
// Fetch member objects from Discord
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const fromMember = await guild.members.fetch(userId);
|
||||
const toMember = await guild.members.fetch(targetUserId);
|
||||
// Fetch member objects from Discord
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const fromMember = await guild.members.fetch(userId);
|
||||
const toMember = await guild.members.fetch(targetUserId);
|
||||
|
||||
// --- Validation Checks ---
|
||||
// 1. Check if a poll is already running for the target user
|
||||
const existingPoll = Object.values(activePolls).find(poll => poll.toUserId === targetUserId);
|
||||
if (existingPoll) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- Validation Checks ---
|
||||
// 1. Check if a poll is already running for the target user
|
||||
const existingPoll = Object.values(activePolls).find((poll) => poll.toUserId === targetUserId);
|
||||
if (existingPoll) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check if the user is already timed out
|
||||
if (toMember.communicationDisabledUntilTimestamp > Date.now()) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `**${toMember.user.globalName}** est déjà timeout.`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 2. Check if the user is already timed out
|
||||
if (toMember.communicationDisabledUntilTimestamp > Date.now()) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `**${toMember.user.globalName}** est déjà timeout.`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Poll Initialization ---
|
||||
const pollId = id; // Use the interaction ID as the unique poll ID
|
||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
||||
// --- Poll Initialization ---
|
||||
const pollId = id; // Use the interaction ID as the unique poll ID
|
||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
||||
|
||||
// Calculate required votes
|
||||
const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
|
||||
const requiredMajority = Math.max(
|
||||
parseInt(process.env.MIN_VOTES, 10),
|
||||
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1
|
||||
);
|
||||
// Calculate required votes
|
||||
const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
|
||||
const requiredMajority = Math.max(
|
||||
parseInt(process.env.MIN_VOTES, 10),
|
||||
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1,
|
||||
);
|
||||
|
||||
// Store poll data in the active state
|
||||
activePolls[pollId] = {
|
||||
id: userId,
|
||||
username: fromMember.user.globalName,
|
||||
toUserId: targetUserId,
|
||||
toUsername: toMember.user.globalName,
|
||||
time: time,
|
||||
time_display: formatTime(time),
|
||||
for: 0,
|
||||
against: 0,
|
||||
voters: [],
|
||||
channelId: channel_id,
|
||||
endpoint: webhookEndpoint,
|
||||
endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000,
|
||||
requiredMajority: requiredMajority,
|
||||
};
|
||||
// Store poll data in the active state
|
||||
activePolls[pollId] = {
|
||||
id: userId,
|
||||
username: fromMember.user.globalName,
|
||||
toUserId: targetUserId,
|
||||
toUsername: toMember.user.globalName,
|
||||
time: time,
|
||||
time_display: formatTime(time),
|
||||
for: 0,
|
||||
against: 0,
|
||||
voters: [],
|
||||
channelId: channel_id,
|
||||
endpoint: webhookEndpoint,
|
||||
endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000,
|
||||
requiredMajority: requiredMajority,
|
||||
};
|
||||
|
||||
// --- Set up Countdown Interval ---
|
||||
const countdownInterval = setInterval(async () => {
|
||||
const poll = activePolls[pollId];
|
||||
// --- Set up Countdown Interval ---
|
||||
const countdownInterval = setInterval(async () => {
|
||||
const poll = activePolls[pollId];
|
||||
|
||||
// If poll no longer exists, clear the interval
|
||||
if (!poll) {
|
||||
clearInterval(countdownInterval);
|
||||
return;
|
||||
}
|
||||
// If poll no longer exists, clear the interval
|
||||
if (!poll) {
|
||||
clearInterval(countdownInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||
|
||||
// --- Poll Expiration Logic ---
|
||||
if (remaining === 0) {
|
||||
clearInterval(countdownInterval);
|
||||
// --- Poll Expiration Logic ---
|
||||
if (remaining === 0) {
|
||||
clearInterval(countdownInterval);
|
||||
|
||||
const votersList = poll.voters.map(voterId => {
|
||||
const user = getUser.get(voterId);
|
||||
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
|
||||
}).join('\n');
|
||||
const votersList = poll.voters
|
||||
.map((voterId) => {
|
||||
const user = getUser.get(voterId);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
try {
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
|
||||
description: `Il manquait **${votesNeeded}** vote(s).`,
|
||||
fields: [{
|
||||
name: 'Pour',
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
}],
|
||||
color: 0xFF4444, // Red for failure
|
||||
}],
|
||||
components: [], // Remove buttons
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating failed poll message:', err);
|
||||
}
|
||||
try {
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
|
||||
description: `Il manquait **${votesNeeded}** vote(s).`,
|
||||
fields: [
|
||||
{
|
||||
name: "Pour",
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
color: 0xff4444, // Red for failure
|
||||
},
|
||||
],
|
||||
components: [], // Remove buttons
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating failed poll message:", err);
|
||||
}
|
||||
|
||||
// Clean up the poll from active state
|
||||
delete activePolls[pollId];
|
||||
io.emit('poll-update'); // Notify frontend
|
||||
return;
|
||||
}
|
||||
// Clean up the poll from active state
|
||||
delete activePolls[pollId];
|
||||
io.emit("poll-update"); // Notify frontend
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Periodic Update Logic ---
|
||||
// Update the message every second with the new countdown
|
||||
try {
|
||||
const votersList = poll.voters.map(voterId => {
|
||||
const user = getUser.get(voterId);
|
||||
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
|
||||
}).join('\n');
|
||||
// --- Periodic Update Logic ---
|
||||
// Update the message every second with the new countdown
|
||||
try {
|
||||
const votersList = poll.voters
|
||||
.map((voterId) => {
|
||||
const user = getUser.get(voterId);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: 'Vote de Timeout',
|
||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||
fields: [{
|
||||
name: 'Pour',
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Temps restant',
|
||||
value: `⏳ ${countdownText}`,
|
||||
inline: false,
|
||||
}],
|
||||
color: 0x5865F2, // Discord Blurple
|
||||
}],
|
||||
// Keep the components so people can still vote
|
||||
components: [{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
|
||||
],
|
||||
}],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating countdown:', err);
|
||||
// If the message was deleted, stop trying to update it.
|
||||
if (err.message.includes('Unknown Message')) {
|
||||
clearInterval(countdownInterval);
|
||||
delete activePolls[pollId];
|
||||
io.emit('poll-update');
|
||||
}
|
||||
}
|
||||
}, 2000); // Update every 2 seconds to avoid rate limits
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Vote de Timeout",
|
||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||
fields: [
|
||||
{
|
||||
name: "Pour",
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Temps restant",
|
||||
value: `⏳ ${countdownText}`,
|
||||
inline: false,
|
||||
},
|
||||
],
|
||||
color: 0x5865f2, // Discord Blurple
|
||||
},
|
||||
],
|
||||
// Keep the components so people can still vote
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `vote_for_${pollId}`,
|
||||
label: "Oui ✅",
|
||||
style: ButtonStyleTypes.SUCCESS,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating countdown:", err);
|
||||
// If the message was deleted, stop trying to update it.
|
||||
if (err.message.includes("Unknown Message")) {
|
||||
clearInterval(countdownInterval);
|
||||
delete activePolls[pollId];
|
||||
io.emit("poll-update");
|
||||
}
|
||||
}
|
||||
}, 2000); // Update every 2 seconds to avoid rate limits
|
||||
|
||||
// --- Send Initial Response ---
|
||||
io.emit('poll-update'); // Notify frontend
|
||||
// --- Send Initial Response ---
|
||||
io.emit("poll-update"); // Notify frontend
|
||||
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [{
|
||||
title: 'Vote de Timeout',
|
||||
description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
|
||||
fields: [{
|
||||
name: 'Pour',
|
||||
value: '✅ 0',
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Temps restant',
|
||||
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
|
||||
inline: false,
|
||||
}],
|
||||
color: 0x5865F2,
|
||||
}],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
|
||||
],
|
||||
}],
|
||||
},
|
||||
});
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Vote de Timeout",
|
||||
description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
|
||||
fields: [
|
||||
{
|
||||
name: "Pour",
|
||||
value: "✅ 0",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Temps restant",
|
||||
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
|
||||
inline: false,
|
||||
},
|
||||
],
|
||||
color: 0x5865f2,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `vote_for_${pollId}`,
|
||||
label: "Oui ✅",
|
||||
style: ButtonStyleTypes.SUCCESS,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
} from 'discord-interactions';
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||
|
||||
import { postAPOBuy } from '../../utils/index.js';
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import {getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
|
||||
import { skins } from '../../game/state.js';
|
||||
import { postAPOBuy } from "../../utils/index.js";
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
|
||||
import { skins } from "../../game/state.js";
|
||||
|
||||
/**
|
||||
* Handles the /valorant slash command for opening a "skin case".
|
||||
@@ -17,198 +14,201 @@ import { skins } from '../../game/state.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleValorantCommand(req, res, client) {
|
||||
const { member, token } = req.body;
|
||||
const userId = member.user.id;
|
||||
const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500;
|
||||
const { member, token } = req.body;
|
||||
const userId = member.user.id;
|
||||
const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500;
|
||||
|
||||
try {
|
||||
// --- 1. Verify and process payment ---
|
||||
try {
|
||||
// --- 1. Verify and process payment ---
|
||||
|
||||
const commandUser = getUser.get(userId);
|
||||
if (!commandUser) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (commandUser.coins < valoPrice) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Pas assez de FlopoCoins (${valoPrice} requis).`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
const commandUser = getUser.get(userId);
|
||||
if (!commandUser) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (commandUser.coins < valoPrice) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Pas assez de FlopoCoins (${valoPrice} requis).`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
insertLog.run({
|
||||
id: `${userId}-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: 'VALO_CASE_OPEN',
|
||||
target_user_id: null,
|
||||
coins_amount: -valoPrice,
|
||||
user_new_amount: commandUser.coins - valoPrice,
|
||||
});
|
||||
updateUserCoins.run({
|
||||
id: userId,
|
||||
coins: commandUser.coins - valoPrice,
|
||||
})
|
||||
insertLog.run({
|
||||
id: `${userId}-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: "VALO_CASE_OPEN",
|
||||
target_user_id: null,
|
||||
coins_amount: -valoPrice,
|
||||
user_new_amount: commandUser.coins - valoPrice,
|
||||
});
|
||||
updateUserCoins.run({
|
||||
id: userId,
|
||||
coins: commandUser.coins - valoPrice,
|
||||
});
|
||||
|
||||
// --- 2. Send Initial "Opening" Response ---
|
||||
// Acknowledge the interaction immediately with a loading message.
|
||||
const initialEmbed = new EmbedBuilder()
|
||||
.setTitle('Ouverture de la caisse...')
|
||||
.setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif')
|
||||
.setColor('#F2F3F3');
|
||||
// --- 2. Send Initial "Opening" Response ---
|
||||
// Acknowledge the interaction immediately with a loading message.
|
||||
const initialEmbed = new EmbedBuilder()
|
||||
.setTitle("Ouverture de la caisse...")
|
||||
.setImage("https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif")
|
||||
.setColor("#F2F3F3");
|
||||
|
||||
await res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { embeds: [initialEmbed] },
|
||||
});
|
||||
await res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { embeds: [initialEmbed] },
|
||||
});
|
||||
|
||||
// --- 3. Run the skin reveal logic after a delay ---
|
||||
setTimeout(async () => {
|
||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
||||
try {
|
||||
// --- Skin Selection ---
|
||||
const availableSkins = getAllAvailableSkins.all();
|
||||
if (availableSkins.length === 0) {
|
||||
throw new Error("No available skins to award.");
|
||||
}
|
||||
const dbSkin = availableSkins[Math.floor(Math.random() * availableSkins.length)];
|
||||
const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid);
|
||||
if (!randomSkinData) {
|
||||
throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`);
|
||||
}
|
||||
|
||||
// --- 3. Run the skin reveal logic after a delay ---
|
||||
setTimeout(async () => {
|
||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
||||
try {
|
||||
// --- Skin Selection ---
|
||||
const availableSkins = getAllAvailableSkins.all();
|
||||
if (availableSkins.length === 0) {
|
||||
throw new Error("No available skins to award.");
|
||||
}
|
||||
const dbSkin = availableSkins[Math.floor(Math.random() * availableSkins.length)];
|
||||
const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid);
|
||||
if (!randomSkinData) {
|
||||
throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`);
|
||||
}
|
||||
// --- Randomize Level and Chroma ---
|
||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||
let randomChroma = 1;
|
||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||
// Ensure chroma is at least 1 and not greater than the number of chromas
|
||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||
}
|
||||
|
||||
// --- Randomize Level and Chroma ---
|
||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||
let randomChroma = 1;
|
||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||
// Ensure chroma is at least 1 and not greater than the number of chromas
|
||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||
}
|
||||
// --- Calculate Price ---
|
||||
const calculatePrice = () => {
|
||||
let result = parseFloat(dbSkin.basePrice);
|
||||
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
|
||||
result *= 1 + randomChroma / 4;
|
||||
return parseFloat(result.toFixed(0));
|
||||
};
|
||||
const finalPrice = calculatePrice();
|
||||
|
||||
// --- Calculate Price ---
|
||||
const calculatePrice = () => {
|
||||
let result = parseFloat(dbSkin.basePrice);
|
||||
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
|
||||
result *= (1 + (randomChroma / 4));
|
||||
return parseFloat(result.toFixed(0));
|
||||
};
|
||||
const finalPrice = calculatePrice();
|
||||
// --- Update Database ---
|
||||
await updateSkin.run({
|
||||
uuid: randomSkinData.uuid,
|
||||
user_id: userId,
|
||||
currentLvl: randomLevel,
|
||||
currentChroma: randomChroma,
|
||||
currentPrice: finalPrice,
|
||||
});
|
||||
|
||||
// --- Update Database ---
|
||||
await updateSkin.run({
|
||||
uuid: randomSkinData.uuid,
|
||||
user_id: userId,
|
||||
currentLvl: randomLevel,
|
||||
currentChroma: randomChroma,
|
||||
currentPrice: finalPrice,
|
||||
});
|
||||
// --- Prepare Final Embed and Components ---
|
||||
const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice);
|
||||
const components = buildComponents(randomSkinData, randomLevel, randomChroma);
|
||||
|
||||
// --- Prepare Final Embed and Components ---
|
||||
const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice);
|
||||
const components = buildComponents(randomSkinData, randomLevel, randomChroma);
|
||||
|
||||
// --- Edit the Original Message with the Result ---
|
||||
await DiscordRequest(webhookEndpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [finalEmbed],
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (revealError) {
|
||||
console.error('Error during skin reveal:', revealError);
|
||||
// Inform the user that something went wrong
|
||||
await DiscordRequest(webhookEndpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
content: "Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.",
|
||||
embeds: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 5000); // 5-second delay for suspense
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling /valorant command:', error);
|
||||
// This catches errors from the initial interaction, e.g., the payment API call.
|
||||
return res.status(500).json({ error: 'Failed to initiate the case opening.' });
|
||||
}
|
||||
// --- Edit the Original Message with the Result ---
|
||||
await DiscordRequest(webhookEndpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [finalEmbed],
|
||||
components: components,
|
||||
},
|
||||
});
|
||||
} catch (revealError) {
|
||||
console.error("Error during skin reveal:", revealError);
|
||||
// Inform the user that something went wrong
|
||||
await DiscordRequest(webhookEndpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
content:
|
||||
"Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.",
|
||||
embeds: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 5000); // 5-second delay for suspense
|
||||
} catch (error) {
|
||||
console.error("Error handling /valorant command:", error);
|
||||
// This catches errors from the initial interaction, e.g., the payment API call.
|
||||
return res.status(500).json({ error: "Failed to initiate the case opening." });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
/** Builds the final embed to display the won skin. */
|
||||
function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
|
||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||
|
||||
const getChromaName = () => {
|
||||
if (chroma > 1) {
|
||||
const name = selectedChromaData.displayName?.replace(/[\r\n]+/g, ' ').replace(skinData.displayName, '').trim();
|
||||
const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : (name || 'Chroma Inconnu');
|
||||
}
|
||||
return 'Base';
|
||||
};
|
||||
const getChromaName = () => {
|
||||
if (chroma > 1) {
|
||||
const name = selectedChromaData.displayName
|
||||
?.replace(/[\r\n]+/g, " ")
|
||||
.replace(skinData.displayName, "")
|
||||
.trim();
|
||||
const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name || "Chroma Inconnu";
|
||||
}
|
||||
return "Base";
|
||||
};
|
||||
|
||||
const getImageUrl = () => {
|
||||
if (level === skinData.levels.length) {
|
||||
return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon;
|
||||
}
|
||||
const levelData = skinData.levels[level - 1];
|
||||
return levelData?.displayIcon || skinData.displayIcon;
|
||||
};
|
||||
const getImageUrl = () => {
|
||||
if (level === skinData.levels.length) {
|
||||
return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon;
|
||||
}
|
||||
const levelData = skinData.levels[level - 1];
|
||||
return levelData?.displayIcon || skinData.displayIcon;
|
||||
};
|
||||
|
||||
const lvlText = (level >= 1 ? '1️⃣' : '') +
|
||||
(level >= 2 ? '2️⃣' : '') +
|
||||
(level >= 3 ? '3️⃣' : '') +
|
||||
(level >= 4 ? '4️⃣' : '') +
|
||||
(level >= 5 ? '5️⃣' : '') +
|
||||
(level >= 6 ? '6️⃣' : '') +
|
||||
'◾'.repeat(skinData.levels.length - level);
|
||||
const chromaText = '💠'.repeat(chroma) + '◾'.repeat(skinData.chromas.length - chroma);
|
||||
const lvlText =
|
||||
(level >= 1 ? "1️⃣" : "") +
|
||||
(level >= 2 ? "2️⃣" : "") +
|
||||
(level >= 3 ? "3️⃣" : "") +
|
||||
(level >= 4 ? "4️⃣" : "") +
|
||||
(level >= 5 ? "5️⃣" : "") +
|
||||
(level >= 6 ? "6️⃣" : "") +
|
||||
"◾".repeat(skinData.levels.length - level);
|
||||
const chromaText = "💠".repeat(chroma) + "◾".repeat(skinData.chromas.length - chroma);
|
||||
|
||||
return new EmbedBuilder()
|
||||
.setTitle(`${skinData.displayName} | ${getChromaName()}`)
|
||||
.setDescription(dbSkin.tierText)
|
||||
.setColor(`#${dbSkin.tierColor}`)
|
||||
.setImage(getImageUrl())
|
||||
.setFields([
|
||||
{ name: 'Lvl', value: lvlText || 'N/A', inline: true },
|
||||
{ name: 'Chroma', value: chromaText || 'N/A', inline: true },
|
||||
{ name: 'Prix', value: `**${price}** <:vp:1362964205808128122>`, inline: true },
|
||||
])
|
||||
.setFooter({ text: 'Skin ajouté à votre inventaire !' });
|
||||
return new EmbedBuilder()
|
||||
.setTitle(`${skinData.displayName} | ${getChromaName()}`)
|
||||
.setDescription(dbSkin.tierText)
|
||||
.setColor(`#${dbSkin.tierColor}`)
|
||||
.setImage(getImageUrl())
|
||||
.setFields([
|
||||
{ name: "Lvl", value: lvlText || "N/A", inline: true },
|
||||
{ name: "Chroma", value: chromaText || "N/A", inline: true },
|
||||
{
|
||||
name: "Prix",
|
||||
value: `**${price}** <:vp:1362964205808128122>`,
|
||||
inline: true,
|
||||
},
|
||||
])
|
||||
.setFooter({ text: "Skin ajouté à votre inventaire !" });
|
||||
}
|
||||
|
||||
/** Builds the action row with a video button if a video is available. */
|
||||
function buildComponents(skinData, level, chroma) {
|
||||
const selectedLevelData = skinData.levels[level - 1] || {};
|
||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||
const selectedLevelData = skinData.levels[level - 1] || {};
|
||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||
|
||||
let videoUrl = null;
|
||||
if (level === skinData.levels.length) {
|
||||
videoUrl = selectedChromaData.streamedVideo;
|
||||
}
|
||||
videoUrl = videoUrl || selectedLevelData.streamedVideo;
|
||||
let videoUrl = null;
|
||||
if (level === skinData.levels.length) {
|
||||
videoUrl = selectedChromaData.streamedVideo;
|
||||
}
|
||||
videoUrl = videoUrl || selectedLevelData.streamedVideo;
|
||||
|
||||
if (videoUrl) {
|
||||
return [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel('🎬 Aperçu Vidéo')
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(videoUrl)
|
||||
)
|
||||
];
|
||||
}
|
||||
return []; // Return an empty array if no video is available
|
||||
if (videoUrl) {
|
||||
return [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl),
|
||||
),
|
||||
];
|
||||
}
|
||||
return []; // Return an empty array if no video is available
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
InteractionResponseFlags,
|
||||
} from 'discord-interactions';
|
||||
InteractionResponseType,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
InteractionResponseFlags,
|
||||
} from "discord-interactions";
|
||||
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import { activeInventories, skins } from '../../game/state.js';
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activeInventories, skins } from "../../game/state.js";
|
||||
|
||||
/**
|
||||
* Handles navigation button clicks (Previous/Next) for the inventory embed.
|
||||
@@ -15,144 +15,167 @@ import { activeInventories, skins } from '../../game/state.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleInventoryNav(req, res, client) {
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
|
||||
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id
|
||||
const [direction, page, interactionId] = custom_id.split('_');
|
||||
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id
|
||||
const [direction, page, interactionId] = custom_id.split("_");
|
||||
|
||||
// --- 1. Retrieve the interactive session ---
|
||||
const inventorySession = activeInventories[interactionId];
|
||||
// --- 1. Retrieve the interactive session ---
|
||||
const inventorySession = activeInventories[interactionId];
|
||||
|
||||
// --- 2. Validation Checks ---
|
||||
if (!inventorySession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- 2. Validation Checks ---
|
||||
if (!inventorySession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the user clicking the button is the one who initiated the command
|
||||
if (inventorySession.userId !== member.user.id) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Ensure the user clicking the button is the one who initiated the command
|
||||
if (inventorySession.userId !== member.user.id) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Update Page Number ---
|
||||
const { amount } = inventorySession;
|
||||
if (direction === "next") {
|
||||
inventorySession.page = (inventorySession.page + 1) % amount;
|
||||
} else if (direction === "prev") {
|
||||
inventorySession.page = (inventorySession.page - 1 + amount) % amount;
|
||||
}
|
||||
|
||||
// --- 3. Update Page Number ---
|
||||
const { amount } = inventorySession;
|
||||
if (direction === 'next') {
|
||||
inventorySession.page = (inventorySession.page + 1) % amount;
|
||||
} else if (direction === 'prev') {
|
||||
inventorySession.page = (inventorySession.page - 1 + amount) % amount;
|
||||
}
|
||||
try {
|
||||
// --- 4. Rebuild Embed with New Page Content ---
|
||||
const { page, inventorySkins } = inventorySession;
|
||||
const currentSkin = inventorySkins[page];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const targetMember = await guild.members.fetch(inventorySession.akhyId);
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
|
||||
try {
|
||||
// --- 4. Rebuild Embed with New Page Content ---
|
||||
const { page, inventorySkins } = inventorySession;
|
||||
const currentSkin = inventorySkins[page];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||
}
|
||||
return result || "N/A";
|
||||
};
|
||||
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const targetMember = await guild.members.fetch(inventorySession.akhyId);
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName
|
||||
.replace(/[\r\n]+/g, " ")
|
||||
.replace(skinInfo.displayName, "")
|
||||
.trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return "Base";
|
||||
};
|
||||
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? '💠 ' : '◾ ';
|
||||
}
|
||||
return result || 'N/A';
|
||||
};
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName.replace(/[\r\n]+/g, ' ').replace(skinInfo.displayName, '').trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return 'Base';
|
||||
};
|
||||
// --- 5. Rebuild Components (Buttons) ---
|
||||
let components = [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `prev_page_${interactionId}`,
|
||||
label: "⏮️ Préc.",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `next_page_${interactionId}`,
|
||||
label: "Suiv. ⏭️",
|
||||
style: ButtonStyleTypes.SECONDARY,
|
||||
},
|
||||
];
|
||||
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
const isUpgradable =
|
||||
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Conditionally add the upgrade button
|
||||
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 5. Rebuild Components (Buttons) ---
|
||||
let components = [
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
||||
];
|
||||
// --- 6. Send PATCH Request to Update the Message ---
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{
|
||||
type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
|
||||
label: "Voir sur FlopoSite",
|
||||
style: ButtonStyleTypes.LINK,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Conditionally add the upgrade button
|
||||
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 6. Send PATCH Request to Update the Message ---
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
||||
footer: { text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` },
|
||||
fields: [{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
}],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
}],
|
||||
components: [{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{ type: MessageComponentTypes.ACTION_ROW,
|
||||
components: [{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
|
||||
label: 'Voir sur FlopoSite',
|
||||
style: ButtonStyleTypes.LINK,}]
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
// --- 7. Acknowledge the Interaction ---
|
||||
// This tells Discord the interaction was received, and since the message is already updated,
|
||||
// no further action is needed.
|
||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling inventory navigation:', error);
|
||||
// In case of an error, we should still acknowledge the interaction to prevent it from failing.
|
||||
// We can send a silent, ephemeral error message.
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Une erreur est survenue lors de la mise à jour de l\'inventaire.',
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- 7. Acknowledge the Interaction ---
|
||||
// This tells Discord the interaction was received, and since the message is already updated,
|
||||
// no further action is needed.
|
||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
} catch (error) {
|
||||
console.error("Error handling inventory navigation:", error);
|
||||
// In case of an error, we should still acknowledge the interaction to prevent it from failing.
|
||||
// We can send a silent, ephemeral error message.
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Une erreur est survenue lors de la mise à jour de l'inventaire.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
} from 'discord-interactions';
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import { activePolls } from '../../game/state.js';
|
||||
import { getSocketIo } from '../../server/socket.js';
|
||||
import { getUser } from '../../database/index.js';
|
||||
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activePolls } from "../../game/state.js";
|
||||
import { getSocketIo } from "../../server/socket.js";
|
||||
import { getUser } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles clicks on the 'Yes' or 'No' buttons of a timeout poll.
|
||||
@@ -13,164 +10,175 @@ import { getUser } from '../../database/index.js';
|
||||
* @param {object} res - The Express response object.
|
||||
*/
|
||||
export async function handlePollVote(req, res) {
|
||||
const io = getSocketIo();
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
const io = getSocketIo();
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
|
||||
// --- 1. Parse Component ID ---
|
||||
const [_, voteType, pollId] = custom_id.split('_'); // e.g., ['vote', 'for', '12345...']
|
||||
const isVotingFor = voteType === 'for';
|
||||
// --- 1. Parse Component ID ---
|
||||
const [_, voteType, pollId] = custom_id.split("_"); // e.g., ['vote', 'for', '12345...']
|
||||
const isVotingFor = voteType === "for";
|
||||
|
||||
// --- 2. Retrieve Poll and Validate ---
|
||||
const poll = activePolls[pollId];
|
||||
const voterId = member.user.id;
|
||||
// --- 2. Retrieve Poll and Validate ---
|
||||
const poll = activePolls[pollId];
|
||||
const voterId = member.user.id;
|
||||
|
||||
if (!poll) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Ce sondage de timeout n'est plus actif.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!poll) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Ce sondage de timeout n'est plus actif.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the voter has the required role
|
||||
if (!member.roles.includes(process.env.VOTING_ROLE_ID)) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous n'avez pas le rôle requis pour participer à ce vote.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Check if the voter has the required role
|
||||
if (!member.roles.includes(process.env.VOTING_ROLE_ID)) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous n'avez pas le rôle requis pour participer à ce vote.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent user from voting on themselves
|
||||
if (poll.toUserId === voterId) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas voter pour vous-même.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Prevent user from voting on themselves
|
||||
if (poll.toUserId === voterId) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas voter pour vous-même.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent double voting
|
||||
if (poll.voters.includes(voterId)) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Vous avez déjà voté pour ce sondage.',
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Prevent double voting
|
||||
if (poll.voters.includes(voterId)) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous avez déjà voté pour ce sondage.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Record the Vote ---
|
||||
poll.voters.push(voterId);
|
||||
if (isVotingFor) {
|
||||
poll.for++;
|
||||
} else {
|
||||
poll.against++;
|
||||
}
|
||||
// --- 3. Record the Vote ---
|
||||
poll.voters.push(voterId);
|
||||
if (isVotingFor) {
|
||||
poll.for++;
|
||||
} else {
|
||||
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 ---
|
||||
if (isVotingFor && poll.for >= poll.requiredMajority) {
|
||||
// --- SUCCESS CASE: MAJORITY REACHED ---
|
||||
|
||||
// --- 4. Check for Majority ---
|
||||
if (isVotingFor && poll.for >= poll.requiredMajority) {
|
||||
// --- SUCCESS CASE: MAJORITY REACHED ---
|
||||
// a. Update the poll message to show success
|
||||
try {
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Vote Terminé - Timeout Appliqué !",
|
||||
description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`,
|
||||
fields: [
|
||||
{
|
||||
name: "Votes Pour",
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
color: 0x22a55b, // Green for success
|
||||
},
|
||||
],
|
||||
components: [], // Remove buttons
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating final poll message:", err);
|
||||
}
|
||||
|
||||
// a. Update the poll message to show success
|
||||
try {
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: 'Vote Terminé - Timeout Appliqué !',
|
||||
description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`,
|
||||
fields: [{ name: 'Votes Pour', value: `✅ ${poll.for}\n${votersList}`, inline: true }],
|
||||
color: 0x22A55B, // Green for success
|
||||
}],
|
||||
components: [], // Remove buttons
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating final poll message:', err);
|
||||
}
|
||||
// b. Execute the timeout via Discord API
|
||||
try {
|
||||
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
|
||||
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
|
||||
await DiscordRequest(endpointTimeout, {
|
||||
method: "PATCH",
|
||||
body: { communication_disabled_until: timeoutUntil },
|
||||
});
|
||||
|
||||
// b. Execute the timeout via Discord API
|
||||
try {
|
||||
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
|
||||
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
|
||||
await DiscordRequest(endpointTimeout, {
|
||||
method: 'PATCH',
|
||||
body: { communication_disabled_until: timeoutUntil },
|
||||
});
|
||||
// c. Send a public confirmation message and clean up
|
||||
delete activePolls[pollId];
|
||||
io.emit("poll-update");
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error timing out user:", err);
|
||||
delete activePolls[pollId];
|
||||
io.emit("poll-update");
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `La majorité a été atteinte, mais une erreur est survenue lors de l'application du timeout sur <@${poll.toUserId}>.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// --- PENDING CASE: NO MAJORITY YET ---
|
||||
|
||||
// c. Send a public confirmation message and clean up
|
||||
delete activePolls[pollId];
|
||||
io.emit('poll-update');
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`,
|
||||
},
|
||||
});
|
||||
// a. Send an ephemeral acknowledgment to the voter
|
||||
res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Votre vote a été enregistré ! ✅",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error timing out user:', err);
|
||||
delete activePolls[pollId];
|
||||
io.emit('poll-update');
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `La majorité a été atteinte, mais une erreur est survenue lors de l'application du timeout sur <@${poll.toUserId}>.`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// --- PENDING CASE: NO MAJORITY YET ---
|
||||
// b. Update the original poll message asynchronously (no need to await)
|
||||
// The main countdown interval will also handle this, but this provides a faster update.
|
||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||
|
||||
// a. Send an ephemeral acknowledgment to the voter
|
||||
res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Votre vote a été enregistré ! ✅',
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
|
||||
// b. Update the original poll message asynchronously (no need to await)
|
||||
// The main countdown interval will also handle this, but this provides a faster update.
|
||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||
|
||||
DiscordRequest(poll.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: 'Vote de Timeout',
|
||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||
fields: [{
|
||||
name: 'Pour',
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Temps restant',
|
||||
value: `⏳ ${countdownText}`,
|
||||
inline: false,
|
||||
}],
|
||||
color: 0x5865F2,
|
||||
}],
|
||||
// Keep the original components so people can still vote
|
||||
components: req.body.message.components,
|
||||
},
|
||||
}).catch(err => console.error("Error updating poll after vote:", err));
|
||||
}
|
||||
DiscordRequest(poll.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Vote de Timeout",
|
||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||
fields: [
|
||||
{
|
||||
name: "Pour",
|
||||
value: `✅ ${poll.for}\n${votersList}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Temps restant",
|
||||
value: `⏳ ${countdownText}`,
|
||||
inline: false,
|
||||
},
|
||||
],
|
||||
color: 0x5865f2,
|
||||
},
|
||||
],
|
||||
// Keep the original components so people can still vote
|
||||
components: req.body.message.components,
|
||||
},
|
||||
}).catch((err) => console.error("Error updating poll after vote:", err));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from "discord-interactions";
|
||||
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import { activeSearchs, skins } from '../../game/state.js';
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activeSearchs, skins } from "../../game/state.js";
|
||||
|
||||
/**
|
||||
* Handles navigation button clicks (Previous/Next) for the search results embed.
|
||||
@@ -15,107 +15,110 @@ import { activeSearchs, skins } from '../../game/state.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleSearchNav(req, res, client) {
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
|
||||
// Extract direction and the original interaction ID from the custom_id
|
||||
const [direction, _, page, interactionId] = custom_id.split('_'); // e.g., ['next', 'search', 'page', '123...']
|
||||
// Extract direction and the original interaction ID from the custom_id
|
||||
const [direction, _, page, interactionId] = custom_id.split("_"); // e.g., ['next', 'search', 'page', '123...']
|
||||
|
||||
// --- 1. Retrieve the interactive session ---
|
||||
const searchSession = activeSearchs[interactionId];
|
||||
// --- 1. Retrieve the interactive session ---
|
||||
const searchSession = activeSearchs[interactionId];
|
||||
|
||||
// --- 2. Validation Checks ---
|
||||
if (!searchSession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// --- 2. Validation Checks ---
|
||||
if (!searchSession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the user clicking the button is the one who initiated the command
|
||||
if (searchSession.userId !== member.user.id) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Ensure the user clicking the button is the one who initiated the command
|
||||
if (searchSession.userId !== member.user.id) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Update Page Number ---
|
||||
const { amount } = searchSession;
|
||||
if (direction === 'next') {
|
||||
searchSession.page = (searchSession.page + 1) % amount;
|
||||
} else if (direction === 'prev') {
|
||||
searchSession.page = (searchSession.page - 1 + amount) % amount;
|
||||
}
|
||||
// --- 3. Update Page Number ---
|
||||
const { amount } = searchSession;
|
||||
if (direction === "next") {
|
||||
searchSession.page = (searchSession.page + 1) % amount;
|
||||
} else if (direction === "prev") {
|
||||
searchSession.page = (searchSession.page - 1 + amount) % amount;
|
||||
}
|
||||
|
||||
try {
|
||||
// --- 4. Rebuild Embed with New Page Content ---
|
||||
const { page, resultSkins, searchValue } = searchSession;
|
||||
const currentSkin = resultSkins[page];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
try {
|
||||
// --- 4. Rebuild Embed with New Page Content ---
|
||||
const { page, resultSkins, searchValue } = searchSession;
|
||||
const currentSkin = resultSkins[page];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
|
||||
// Fetch owner details if the skin is owned
|
||||
let ownerText = '';
|
||||
if (currentSkin.user_id) {
|
||||
try {
|
||||
const owner = await client.users.fetch(currentSkin.user_id);
|
||||
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||
ownerText = '| Appartenant à un utilisateur inconnu';
|
||||
}
|
||||
}
|
||||
// Fetch owner details if the skin is owned
|
||||
let ownerText = "";
|
||||
if (currentSkin.user_id) {
|
||||
try {
|
||||
const owner = await client.users.fetch(currentSkin.user_id);
|
||||
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||
ownerText = "| Appartenant à un utilisateur inconnu";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get the best possible image for the skin
|
||||
const getImageUrl = (skinInfo) => {
|
||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
||||
return skinInfo.displayIcon;
|
||||
};
|
||||
// Helper to get the best possible image for the skin
|
||||
const getImageUrl = (skinInfo) => {
|
||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
||||
return skinInfo.displayIcon;
|
||||
};
|
||||
|
||||
// --- 5. Send PATCH Request to Update the Message ---
|
||||
// Note: The components (buttons) do not change, so we can reuse them from the original message.
|
||||
await DiscordRequest(searchSession.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: 'Résultats de la recherche',
|
||||
description: `🔎 _"${searchValue}"_`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
||||
fields: [{
|
||||
name: `**${currentSkin.displayName}**`,
|
||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||
}],
|
||||
image: { url: getImageUrl(skinData) },
|
||||
footer: { text: `Résultat ${page + 1}/${amount}` },
|
||||
}],
|
||||
components: req.body.message.components, // Reuse existing components
|
||||
},
|
||||
});
|
||||
// --- 5. Send PATCH Request to Update the Message ---
|
||||
// Note: The components (buttons) do not change, so we can reuse them from the original message.
|
||||
await DiscordRequest(searchSession.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Résultats de la recherche",
|
||||
description: `🔎 _"${searchValue}"_`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
fields: [
|
||||
{
|
||||
name: `**${currentSkin.displayName}**`,
|
||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(skinData) },
|
||||
footer: { text: `Résultat ${page + 1}/${amount}` },
|
||||
},
|
||||
],
|
||||
components: req.body.message.components, // Reuse existing components
|
||||
},
|
||||
});
|
||||
|
||||
// --- 6. Acknowledge the Interaction ---
|
||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling search navigation:', error);
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: 'Une erreur est survenue lors de la mise à jour de la recherche.',
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
}
|
||||
});
|
||||
}
|
||||
// --- 6. Acknowledge the Interaction ---
|
||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
} catch (error) {
|
||||
console.error("Error handling search navigation:", error);
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Une erreur est survenue lors de la mise à jour de la recherche.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import {
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from "discord-interactions";
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||
|
||||
import { DiscordRequest } from '../../api/discord.js';
|
||||
import { postAPOBuy } from '../../utils/index.js';
|
||||
import { activeInventories, skins } from '../../game/state.js';
|
||||
import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { postAPOBuy } from "../../utils/index.js";
|
||||
import { activeInventories, skins } from "../../game/state.js";
|
||||
import { getSkin, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
|
||||
|
||||
/**
|
||||
* Handles the click of the 'Upgrade' button on a skin in the inventory.
|
||||
@@ -17,202 +17,228 @@ import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../da
|
||||
* @param {object} res - The Express response object.
|
||||
*/
|
||||
export async function handleUpgradeSkin(req, res) {
|
||||
const { member, data } = req.body;
|
||||
const { custom_id } = data;
|
||||
const { member, data } = req.body;
|
||||
const { custom_id } = data;
|
||||
|
||||
const interactionId = custom_id.replace('upgrade_', '');
|
||||
const userId = member.user.id;
|
||||
const interactionId = custom_id.replace("upgrade_", "");
|
||||
const userId = member.user.id;
|
||||
|
||||
// --- 1. Retrieve Session and Validate ---
|
||||
const inventorySession = activeInventories[interactionId];
|
||||
if (!inventorySession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { content: "Cet affichage d'inventaire a expiré.", flags: InteractionResponseFlags.EPHEMERAL },
|
||||
});
|
||||
}
|
||||
// --- 1. Retrieve Session and Validate ---
|
||||
const inventorySession = activeInventories[interactionId];
|
||||
if (!inventorySession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Cet affichage d'inventaire a expiré.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the user clicking is the inventory owner
|
||||
if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.", flags: InteractionResponseFlags.EPHEMERAL },
|
||||
});
|
||||
}
|
||||
// Ensure the user clicking is the inventory owner
|
||||
if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page];
|
||||
const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid);
|
||||
const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page];
|
||||
const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid);
|
||||
|
||||
if (!skinData || (skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: { content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.", flags: InteractionResponseFlags.EPHEMERAL },
|
||||
});
|
||||
}
|
||||
if (
|
||||
!skinData ||
|
||||
(skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)
|
||||
) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 2. Handle Payment ---
|
||||
const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10;
|
||||
|
||||
// --- 2. Handle Payment ---
|
||||
const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10;
|
||||
const commandUser = getUser.get(userId);
|
||||
|
||||
const commandUser = getUser.get(userId);
|
||||
if (!commandUser) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (commandUser.coins < upgradePrice) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Pas assez de FlopoCoins (${upgradePrice.toFixed(0)} requis).`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!commandUser) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (commandUser.coins < upgradePrice) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `Pas assez de FlopoCoins (${upgradePrice.toFixed(0)} requis).`,
|
||||
flags: InteractionResponseFlags.EPHEMERAL,
|
||||
},
|
||||
});
|
||||
}
|
||||
insertLog.run({
|
||||
id: `${userId}-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: "VALO_SKIN_UPGRADE",
|
||||
target_user_id: null,
|
||||
coins_amount: -upgradePrice.toFixed(0),
|
||||
user_new_amount: commandUser.coins - upgradePrice.toFixed(0),
|
||||
});
|
||||
updateUserCoins.run({
|
||||
id: userId,
|
||||
coins: commandUser.coins - upgradePrice.toFixed(0),
|
||||
});
|
||||
|
||||
insertLog.run({
|
||||
id: `${userId}-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: 'VALO_SKIN_UPGRADE',
|
||||
target_user_id: null,
|
||||
coins_amount: -upgradePrice.toFixed(0),
|
||||
user_new_amount: commandUser.coins - upgradePrice.toFixed(0),
|
||||
});
|
||||
updateUserCoins.run({
|
||||
id: userId,
|
||||
coins: commandUser.coins - upgradePrice.toFixed(0),
|
||||
})
|
||||
// --- 3. Show Loading Animation ---
|
||||
// Acknowledge the click immediately and then edit the message to show a loading state.
|
||||
await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: "Amélioration en cours...",
|
||||
image: {
|
||||
url: "https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif",
|
||||
},
|
||||
color: 0x4f545c,
|
||||
},
|
||||
],
|
||||
components: [],
|
||||
},
|
||||
});
|
||||
|
||||
// --- 3. Show Loading Animation ---
|
||||
// Acknowledge the click immediately and then edit the message to show a loading state.
|
||||
await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
// --- 4. Perform Upgrade Logic ---
|
||||
let succeeded = false;
|
||||
const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length;
|
||||
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [{
|
||||
title: 'Amélioration en cours...',
|
||||
image: { url: 'https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif' },
|
||||
color: 0x4F545C,
|
||||
}],
|
||||
components: [],
|
||||
},
|
||||
});
|
||||
if (isLevelUpgrade) {
|
||||
// Upgrading Level
|
||||
const successProb =
|
||||
1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
||||
if (Math.random() < successProb) {
|
||||
succeeded = true;
|
||||
skinToUpgrade.currentLvl++;
|
||||
}
|
||||
} else {
|
||||
// Upgrading Chroma
|
||||
const successProb =
|
||||
1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
||||
if (Math.random() < successProb) {
|
||||
succeeded = true;
|
||||
skinToUpgrade.currentChroma++;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. Update Database if Successful ---
|
||||
if (succeeded) {
|
||||
const calculatePrice = () => {
|
||||
let result = parseFloat(skinToUpgrade.basePrice);
|
||||
result *= 1 + skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2);
|
||||
result *= 1 + skinToUpgrade.currentChroma / 4;
|
||||
return parseFloat(result.toFixed(0));
|
||||
};
|
||||
skinToUpgrade.currentPrice = calculatePrice();
|
||||
|
||||
// --- 4. Perform Upgrade Logic ---
|
||||
let succeeded = false;
|
||||
const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length;
|
||||
await updateSkin.run({
|
||||
uuid: skinToUpgrade.uuid,
|
||||
user_id: skinToUpgrade.user_id,
|
||||
currentLvl: skinToUpgrade.currentLvl,
|
||||
currentChroma: skinToUpgrade.currentChroma,
|
||||
currentPrice: skinToUpgrade.currentPrice,
|
||||
});
|
||||
// Update the session cache
|
||||
inventorySession.inventorySkins[inventorySession.page] = skinToUpgrade;
|
||||
}
|
||||
|
||||
if (isLevelUpgrade) {
|
||||
// Upgrading Level
|
||||
const successProb = 1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
||||
if (Math.random() < successProb) {
|
||||
succeeded = true;
|
||||
skinToUpgrade.currentLvl++;
|
||||
}
|
||||
} else {
|
||||
// Upgrading Chroma
|
||||
const successProb = 1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
||||
if (Math.random() < successProb) {
|
||||
succeeded = true;
|
||||
skinToUpgrade.currentChroma++;
|
||||
}
|
||||
}
|
||||
// --- 6. Send Final Result ---
|
||||
setTimeout(async () => {
|
||||
// Fetch the latest state of the skin from the database
|
||||
const finalSkinState = getSkin.get(skinToUpgrade.uuid);
|
||||
const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData);
|
||||
const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);
|
||||
|
||||
// --- 5. Update Database if Successful ---
|
||||
if (succeeded) {
|
||||
const calculatePrice = () => {
|
||||
let result = parseFloat(skinToUpgrade.basePrice);
|
||||
result *= (1 + (skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2)));
|
||||
result *= (1 + (skinToUpgrade.currentChroma / 4));
|
||||
return parseFloat(result.toFixed(0));
|
||||
};
|
||||
skinToUpgrade.currentPrice = calculatePrice();
|
||||
|
||||
await updateSkin.run({
|
||||
uuid: skinToUpgrade.uuid,
|
||||
user_id: skinToUpgrade.user_id,
|
||||
currentLvl: skinToUpgrade.currentLvl,
|
||||
currentChroma: skinToUpgrade.currentChroma,
|
||||
currentPrice: skinToUpgrade.currentPrice,
|
||||
});
|
||||
// Update the session cache
|
||||
inventorySession.inventorySkins[inventorySession.page] = skinToUpgrade;
|
||||
}
|
||||
|
||||
|
||||
// --- 6. Send Final Result ---
|
||||
setTimeout(async () => {
|
||||
// Fetch the latest state of the skin from the database
|
||||
const finalSkinState = getSkin.get(skinToUpgrade.uuid);
|
||||
const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData);
|
||||
const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);
|
||||
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
embeds: [finalEmbed],
|
||||
components: finalComponents,
|
||||
},
|
||||
});
|
||||
}, 2000); // Delay for the result to feel more impactful
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [finalEmbed],
|
||||
components: finalComponents,
|
||||
},
|
||||
});
|
||||
}, 2000); // Delay for the result to feel more impactful
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
/** Builds the result embed (success or failure). */
|
||||
function buildFinalEmbed(succeeded, skin, skinData) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(succeeded ? 'Amélioration Réussie ! 🎉' : "L'amélioration a échoué... ❌")
|
||||
.setDescription(`**${skin.displayName}**`)
|
||||
.setImage(skin.displayIcon) // A static image is fine here
|
||||
.setColor(succeeded ? 0x22A55B : 0xED4245);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(succeeded ? "Amélioration Réussie ! 🎉" : "L'amélioration a échoué... ❌")
|
||||
.setDescription(`**${skin.displayName}**`)
|
||||
.setImage(skin.displayIcon) // A static image is fine here
|
||||
.setColor(succeeded ? 0x22a55b : 0xed4245);
|
||||
|
||||
if (succeeded) {
|
||||
embed.addFields(
|
||||
{ name: 'Nouveau Niveau', value: `${skin.currentLvl}/${skinData.levels.length}`, inline: true },
|
||||
{ name: 'Nouveau Chroma', value: `${skin.currentChroma}/${skinData.chromas.length}`, inline: true },
|
||||
{ name: 'Nouvelle Valeur', value: `**${skin.currentPrice} Flopos**`, inline: true }
|
||||
);
|
||||
} else {
|
||||
embed.addFields({ name: 'Statut', value: 'Aucun changement.' });
|
||||
}
|
||||
return embed;
|
||||
if (succeeded) {
|
||||
embed.addFields(
|
||||
{
|
||||
name: "Nouveau Niveau",
|
||||
value: `${skin.currentLvl}/${skinData.levels.length}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Nouveau Chroma",
|
||||
value: `${skin.currentChroma}/${skinData.chromas.length}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Nouvelle Valeur",
|
||||
value: `**${skin.currentPrice} Flopos**`,
|
||||
inline: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
embed.addFields({ name: "Statut", value: "Aucun changement." });
|
||||
}
|
||||
return embed;
|
||||
}
|
||||
|
||||
/** Builds the result components (Retry button or Video link). */
|
||||
function buildFinalComponents(succeeded, skinData, skin, interactionId) {
|
||||
const isMaxed = skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length;
|
||||
const isMaxed = skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length;
|
||||
|
||||
if (isMaxed) return []; // No buttons if maxed out
|
||||
if (isMaxed) return []; // No buttons if maxed out
|
||||
|
||||
const row = new ActionRowBuilder();
|
||||
if (succeeded) {
|
||||
// Check for video on the new level/chroma
|
||||
const levelData = skinData.levels[skin.currentLvl - 1] || {};
|
||||
const chromaData = skinData.chromas[skin.currentChroma - 1] || {};
|
||||
const videoUrl = levelData.streamedVideo || chromaData.streamedVideo;
|
||||
const row = new ActionRowBuilder();
|
||||
if (succeeded) {
|
||||
// Check for video on the new level/chroma
|
||||
const levelData = skinData.levels[skin.currentLvl - 1] || {};
|
||||
const chromaData = skinData.chromas[skin.currentChroma - 1] || {};
|
||||
const videoUrl = levelData.streamedVideo || chromaData.streamedVideo;
|
||||
|
||||
if (videoUrl) {
|
||||
row.addComponents(new ButtonBuilder().setLabel('🎬 Aperçu Vidéo').setStyle(ButtonStyle.Link).setURL(videoUrl));
|
||||
} else {
|
||||
return []; // No button if no video
|
||||
}
|
||||
} else {
|
||||
// Add a "Retry" button
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel('Réessayer 🔄️')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(`upgrade_${interactionId}`)
|
||||
);
|
||||
}
|
||||
return [row];
|
||||
if (videoUrl) {
|
||||
row.addComponents(new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl));
|
||||
} else {
|
||||
return []; // No button if no video
|
||||
}
|
||||
} else {
|
||||
// Add a "Retry" button
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel("Réessayer 🔄️")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(`upgrade_${interactionId}`),
|
||||
);
|
||||
}
|
||||
return [row];
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { handleMessageCreate } from './handlers/messageCreate.js';
|
||||
import { getAkhys, setupCronJobs } from '../utils/index.js';
|
||||
import { handleMessageCreate } from "./handlers/messageCreate.js";
|
||||
import { getAkhys, setupCronJobs } from "../utils/index.js";
|
||||
|
||||
/**
|
||||
* Initializes and attaches all necessary event listeners to the Discord client.
|
||||
@@ -9,44 +9,44 @@ import { getAkhys, setupCronJobs } from '../utils/index.js';
|
||||
* @param {object} io - The Socket.IO server instance for real-time communication.
|
||||
*/
|
||||
export function initializeEvents(client, io) {
|
||||
// --- on 'ready' ---
|
||||
// This event fires once the bot has successfully logged in and is ready to operate.
|
||||
// It's a good place for setup tasks that require the bot to be online.
|
||||
client.once('ready', async () => {
|
||||
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
|
||||
console.log('[Startup] Bot is ready, performing initial data sync...');
|
||||
await getAkhys(client);
|
||||
console.log('[Startup] Setting up scheduled tasks...');
|
||||
setupCronJobs(client, io);
|
||||
console.log('--- FlopoBOT is fully operational ---');
|
||||
});
|
||||
// --- on 'ready' ---
|
||||
// This event fires once the bot has successfully logged in and is ready to operate.
|
||||
// It's a good place for setup tasks that require the bot to be online.
|
||||
client.once("clientReady", async () => {
|
||||
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
|
||||
console.log("[Startup] Bot is ready, performing initial data sync...");
|
||||
await getAkhys(client);
|
||||
console.log("[Startup] Setting up scheduled tasks...");
|
||||
setupCronJobs(client, io);
|
||||
console.log("--- FlopoBOT is fully operational ---");
|
||||
});
|
||||
|
||||
// --- on 'messageCreate' ---
|
||||
// This event fires every time a message is sent in a channel the bot can see.
|
||||
// The logic is delegated to its own dedicated handler for cleanliness.
|
||||
client.on('messageCreate', async (message) => {
|
||||
// We pass the client and io instances to the handler so it has access to them
|
||||
// without needing to import them, preventing potential circular dependencies.
|
||||
await handleMessageCreate(message, client, io);
|
||||
});
|
||||
// --- on 'messageCreate' ---
|
||||
// This event fires every time a message is sent in a channel the bot can see.
|
||||
// The logic is delegated to its own dedicated handler for cleanliness.
|
||||
client.on("messageCreate", async (message) => {
|
||||
// We pass the client and io instances to the handler so it has access to them
|
||||
// without needing to import them, preventing potential circular dependencies.
|
||||
await handleMessageCreate(message, client, io);
|
||||
});
|
||||
|
||||
// --- on 'interactionCreate' (Alternative Method) ---
|
||||
// While we handle interactions via the Express endpoint for scalability and statelessness,
|
||||
// you could also listen for them via the gateway like this.
|
||||
// It's commented out because our current architecture uses the webhook approach.
|
||||
/*
|
||||
// --- on 'interactionCreate' (Alternative Method) ---
|
||||
// While we handle interactions via the Express endpoint for scalability and statelessness,
|
||||
// you could also listen for them via the gateway like this.
|
||||
// It's commented out because our current architecture uses the webhook approach.
|
||||
/*
|
||||
client.on('interactionCreate', async (interaction) => {
|
||||
// Logic to handle interactions would go here if not using a webhook endpoint.
|
||||
});
|
||||
*/
|
||||
|
||||
// You can add more event listeners here as your bot's functionality grows.
|
||||
// For example, listening for new members joining the server:
|
||||
// client.on('guildMemberAdd', (member) => {
|
||||
// console.log(`Welcome to the server, ${member.user.tag}!`);
|
||||
// const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID');
|
||||
// if (welcomeChannel) {
|
||||
// welcomeChannel.send(`Please welcome <@${member.id}> to the server!`);
|
||||
// }
|
||||
// });
|
||||
// You can add more event listeners here as your bot's functionality grows.
|
||||
// For example, listening for new members joining the server:
|
||||
// client.on('guildMemberAdd', (member) => {
|
||||
// console.log(`Welcome to the server, ${member.user.tag}!`);
|
||||
// const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID');
|
||||
// if (welcomeChannel) {
|
||||
// welcomeChannel.send(`Please welcome <@${member.id}> to the server!`);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
import {
|
||||
InteractionType,
|
||||
InteractionResponseType,
|
||||
} from 'discord-interactions';
|
||||
import { InteractionType, InteractionResponseType } from "discord-interactions";
|
||||
|
||||
// --- Command Handlers ---
|
||||
import { handleTimeoutCommand } from '../commands/timeout.js';
|
||||
import { handleInventoryCommand } from '../commands/inventory.js';
|
||||
import { handleValorantCommand } from '../commands/valorant.js';
|
||||
import { handleInfoCommand } from '../commands/info.js';
|
||||
import { handleSkinsCommand } from '../commands/skins.js';
|
||||
import { handleSearchCommand } from '../commands/search.js';
|
||||
import { handleFlopoSiteCommand } from '../commands/floposite.js';
|
||||
import { handleTimeoutCommand } from "../commands/timeout.js";
|
||||
import { handleInventoryCommand } from "../commands/inventory.js";
|
||||
import { handleValorantCommand } from "../commands/valorant.js";
|
||||
import { handleInfoCommand } from "../commands/info.js";
|
||||
import { handleSkinsCommand } from "../commands/skins.js";
|
||||
import { handleSearchCommand } from "../commands/search.js";
|
||||
import { handleFlopoSiteCommand } from "../commands/floposite.js";
|
||||
|
||||
// --- Component Handlers ---
|
||||
import { handlePollVote } from '../components/pollVote.js';
|
||||
import { handleInventoryNav } from '../components/inventoryNav.js';
|
||||
import { handleUpgradeSkin } from '../components/upgradeSkin.js';
|
||||
import { handleSearchNav } from '../components/searchNav.js';
|
||||
import { handlePollVote } from "../components/pollVote.js";
|
||||
import { handleInventoryNav } from "../components/inventoryNav.js";
|
||||
import { handleUpgradeSkin } from "../components/upgradeSkin.js";
|
||||
import { handleSearchNav } from "../components/searchNav.js";
|
||||
|
||||
/**
|
||||
* The main handler for all incoming interactions from Discord.
|
||||
@@ -25,65 +22,64 @@ import { handleSearchNav } from '../components/searchNav.js';
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function handleInteraction(req, res, client) {
|
||||
const { type, data, id } = req.body;
|
||||
const { type, data, id } = req.body;
|
||||
|
||||
try {
|
||||
if (type === InteractionType.PING) {
|
||||
return res.send({ type: InteractionResponseType.PONG });
|
||||
}
|
||||
try {
|
||||
if (type === InteractionType.PING) {
|
||||
return res.send({ type: InteractionResponseType.PONG });
|
||||
}
|
||||
|
||||
if (type === InteractionType.APPLICATION_COMMAND) {
|
||||
const { name } = data;
|
||||
if (type === InteractionType.APPLICATION_COMMAND) {
|
||||
const { name } = data;
|
||||
|
||||
switch (name) {
|
||||
case 'timeout':
|
||||
return await handleTimeoutCommand(req, res, client);
|
||||
case 'inventory':
|
||||
return await handleInventoryCommand(req, res, client, id);
|
||||
case 'valorant':
|
||||
return await handleValorantCommand(req, res, client);
|
||||
case 'info':
|
||||
return await handleInfoCommand(req, res, client);
|
||||
case 'skins':
|
||||
return await handleSkinsCommand(req, res, client);
|
||||
case 'search':
|
||||
return await handleSearchCommand(req, res, client, id);
|
||||
case 'floposite':
|
||||
return await handleFlopoSiteCommand(req, res);
|
||||
default:
|
||||
console.error(`Unknown command: ${name}`);
|
||||
return res.status(400).json({ error: 'Unknown command' });
|
||||
}
|
||||
}
|
||||
switch (name) {
|
||||
case "timeout":
|
||||
return await handleTimeoutCommand(req, res, client);
|
||||
case "inventory":
|
||||
return await handleInventoryCommand(req, res, client, id);
|
||||
case "valorant":
|
||||
return await handleValorantCommand(req, res, client);
|
||||
case "info":
|
||||
return await handleInfoCommand(req, res, client);
|
||||
case "skins":
|
||||
return await handleSkinsCommand(req, res, client);
|
||||
case "search":
|
||||
return await handleSearchCommand(req, res, client, id);
|
||||
case "floposite":
|
||||
return await handleFlopoSiteCommand(req, res);
|
||||
default:
|
||||
console.error(`Unknown command: ${name}`);
|
||||
return res.status(400).json({ error: "Unknown command" });
|
||||
}
|
||||
}
|
||||
|
||||
if (type === InteractionType.MESSAGE_COMPONENT) {
|
||||
const componentId = data.custom_id;
|
||||
if (type === InteractionType.MESSAGE_COMPONENT) {
|
||||
const componentId = data.custom_id;
|
||||
|
||||
if (componentId.startsWith('vote_')) {
|
||||
return await handlePollVote(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith('prev_page') || componentId.startsWith('next_page')) {
|
||||
return await handleInventoryNav(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith('upgrade_')) {
|
||||
return await handleUpgradeSkin(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith('prev_search_page') || componentId.startsWith('next_search_page')) {
|
||||
return await handleSearchNav(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith("vote_")) {
|
||||
return await handlePollVote(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith("prev_page") || componentId.startsWith("next_page")) {
|
||||
return await handleInventoryNav(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith("upgrade_")) {
|
||||
return await handleUpgradeSkin(req, res, client);
|
||||
}
|
||||
if (componentId.startsWith("prev_search_page") || componentId.startsWith("next_search_page")) {
|
||||
return await handleSearchNav(req, res, client);
|
||||
}
|
||||
|
||||
// Fallback for other potential components
|
||||
console.error(`Unknown component ID: ${componentId}`);
|
||||
return res.status(400).json({ error: 'Unknown component' });
|
||||
}
|
||||
// Fallback for other potential components
|
||||
console.error(`Unknown component ID: ${componentId}`);
|
||||
return res.status(400).json({ error: "Unknown component" });
|
||||
}
|
||||
|
||||
// --- Fallback for Unknown Interaction Types ---
|
||||
console.error('Unknown interaction type:', type);
|
||||
return res.status(400).json({ error: 'Unknown interaction type' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling interaction:', error);
|
||||
// Send a generic error response to Discord if something goes wrong
|
||||
return res.status(500).json({ error: 'An internal error occurred' });
|
||||
}
|
||||
// --- Fallback for Unknown Interaction Types ---
|
||||
console.error("Unknown interaction type:", type);
|
||||
return res.status(400).json({ error: "Unknown interaction type" });
|
||||
} catch (error) {
|
||||
console.error("Error handling interaction:", error);
|
||||
// Send a generic error response to Discord if something goes wrong
|
||||
return res.status(500).json({ error: "An internal error occurred" });
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,113 +1,121 @@
|
||||
import 'dotenv/config';
|
||||
import { getTimesChoices } from '../game/various.js';
|
||||
import { capitalize, InstallGlobalCommands } from '../utils/index.js';
|
||||
import "dotenv/config";
|
||||
import { getTimesChoices } from "../game/various.js";
|
||||
import { capitalize, InstallGlobalCommands } from "../utils/index.js";
|
||||
|
||||
function createTimesChoices() {
|
||||
const choices = getTimesChoices();
|
||||
const commandChoices = [];
|
||||
const choices = getTimesChoices();
|
||||
const commandChoices = [];
|
||||
|
||||
for (let choice of choices) {
|
||||
commandChoices.push({
|
||||
name: capitalize(choice.name),
|
||||
value: choice.value?.toString(),
|
||||
});
|
||||
}
|
||||
for (let choice of choices) {
|
||||
commandChoices.push({
|
||||
name: capitalize(choice.name),
|
||||
value: choice.value?.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return commandChoices;
|
||||
return commandChoices;
|
||||
}
|
||||
|
||||
// Timeout vote command
|
||||
const TIMEOUT_COMMAND = {
|
||||
name: 'timeout',
|
||||
description: 'Vote démocratique pour timeout un boug',
|
||||
options: [
|
||||
{
|
||||
type: 6,
|
||||
name: 'akhy',
|
||||
description: 'Qui ?',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
name: 'temps',
|
||||
description: 'Combien de temps ?',
|
||||
required: true,
|
||||
choices: createTimesChoices(),
|
||||
}
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "timeout",
|
||||
description: "Vote démocratique pour timeout un boug",
|
||||
options: [
|
||||
{
|
||||
type: 6,
|
||||
name: "akhy",
|
||||
description: "Qui ?",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
name: "temps",
|
||||
description: "Combien de temps ?",
|
||||
required: true,
|
||||
choices: createTimesChoices(),
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
// Valorant
|
||||
const VALORANT_COMMAND = {
|
||||
name: 'valorant',
|
||||
description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "valorant",
|
||||
description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
// Own inventory command
|
||||
const INVENTORY_COMMAND = {
|
||||
name: 'inventory',
|
||||
description: 'Voir inventaire',
|
||||
options: [
|
||||
{
|
||||
type: 6,
|
||||
name: 'akhy',
|
||||
description: 'Qui ?',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "inventory",
|
||||
description: "Voir inventaire",
|
||||
options: [
|
||||
{
|
||||
type: 6,
|
||||
name: "akhy",
|
||||
description: "Qui ?",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
const INFO_COMMAND = {
|
||||
name: 'info',
|
||||
description: 'Qui est time out ?',
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "info",
|
||||
description: "Qui est time out ?",
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
const SKINS_COMMAND = {
|
||||
name: 'skins',
|
||||
description: 'Le top 10 des skins les plus chers.',
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "skins",
|
||||
description: "Le top 10 des skins les plus chers.",
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
const SITE_COMMAND = {
|
||||
name: 'floposite',
|
||||
description: 'Lien vers FlopoSite',
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "floposite",
|
||||
description: "Lien vers FlopoSite",
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
const SEARCH_SKIN_COMMAND = {
|
||||
name: 'search',
|
||||
description: 'Chercher un skin',
|
||||
options: [
|
||||
{
|
||||
type: 3,
|
||||
name: 'recherche',
|
||||
description: 'Tu cherches quoi ?',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
}
|
||||
name: "search",
|
||||
description: "Chercher un skin",
|
||||
options: [
|
||||
{
|
||||
type: 3,
|
||||
name: "recherche",
|
||||
description: "Tu cherches quoi ?",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
integration_types: [0, 1],
|
||||
contexts: [0, 2],
|
||||
};
|
||||
|
||||
const ALL_COMMANDS = [TIMEOUT_COMMAND, INVENTORY_COMMAND, VALORANT_COMMAND, INFO_COMMAND, SKINS_COMMAND, SEARCH_SKIN_COMMAND, SITE_COMMAND];
|
||||
const ALL_COMMANDS = [
|
||||
TIMEOUT_COMMAND,
|
||||
INVENTORY_COMMAND,
|
||||
VALORANT_COMMAND,
|
||||
INFO_COMMAND,
|
||||
SKINS_COMMAND,
|
||||
SEARCH_SKIN_COMMAND,
|
||||
SITE_COMMAND,
|
||||
];
|
||||
|
||||
export function registerCommands() {
|
||||
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
|
||||
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
|
||||
}
|
||||
@@ -1,190 +1,381 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
|
||||
export const flopoDB = new Database('flopobot.db');
|
||||
export const flopoDB = new Database("flopobot.db");
|
||||
|
||||
export const stmtUsers = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
globalName TEXT,
|
||||
warned BOOLEAN DEFAULT 0,
|
||||
warns INTEGER DEFAULT 0,
|
||||
allTimeWarns INTEGER DEFAULT 0,
|
||||
totalRequests INTEGER DEFAULT 0,
|
||||
coins INTEGER DEFAULT 0,
|
||||
dailyQueried BOOLEAN DEFAULT 0,
|
||||
avatarUrl TEXT DEFAULT NULL,
|
||||
isAkhy BOOLEAN DEFAULT 0
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
globalName TEXT,
|
||||
warned BOOLEAN DEFAULT 0,
|
||||
warns INTEGER DEFAULT 0,
|
||||
allTimeWarns INTEGER DEFAULT 0,
|
||||
totalRequests INTEGER DEFAULT 0,
|
||||
coins INTEGER DEFAULT 0,
|
||||
dailyQueried BOOLEAN DEFAULT 0,
|
||||
avatarUrl TEXT DEFAULT NULL,
|
||||
isAkhy BOOLEAN DEFAULT 0
|
||||
)
|
||||
`);
|
||||
stmtUsers.run();
|
||||
export const stmtSkins = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS skins (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
displayName TEXT,
|
||||
contentTierUuid TEXT,
|
||||
displayIcon TEXT,
|
||||
user_id TEXT REFERENCES users,
|
||||
tierRank TEXT,
|
||||
tierColor TEXT,
|
||||
tierText TEXT,
|
||||
basePrice TEXT,
|
||||
currentLvl INTEGER DEFAULT NULL,
|
||||
currentChroma INTEGER DEFAULT NULL,
|
||||
currentPrice INTEGER DEFAULT NULL,
|
||||
maxPrice INTEGER DEFAULT NULL
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS skins
|
||||
(
|
||||
uuid TEXT PRIMARY KEY,
|
||||
displayName TEXT,
|
||||
contentTierUuid TEXT,
|
||||
displayIcon TEXT,
|
||||
user_id TEXT REFERENCES users,
|
||||
tierRank TEXT,
|
||||
tierColor TEXT,
|
||||
tierText TEXT,
|
||||
basePrice TEXT,
|
||||
currentLvl INTEGER DEFAULT NULL,
|
||||
currentChroma INTEGER DEFAULT NULL,
|
||||
currentPrice 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 updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id');
|
||||
export const updateUserAvatar = flopoDB.prepare('UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id');
|
||||
export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`);
|
||||
export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0`);
|
||||
export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id');
|
||||
export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?');
|
||||
export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC');
|
||||
export const getAllAkhys = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC');
|
||||
export const insertUser = flopoDB.prepare(
|
||||
`INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
|
||||
VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
|
||||
);
|
||||
export const updateUser = flopoDB.prepare(
|
||||
`UPDATE users
|
||||
SET warned = @warned,
|
||||
warns = @warns,
|
||||
allTimeWarns = @allTimeWarns,
|
||||
totalRequests = @totalRequests
|
||||
WHERE id = @id`,
|
||||
);
|
||||
export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id");
|
||||
export const queryDailyReward = flopoDB.prepare(`UPDATE users
|
||||
SET dailyQueried = 1
|
||||
WHERE id = ?`);
|
||||
export const resetDailyReward = flopoDB.prepare(`UPDATE users
|
||||
SET dailyQueried = 0`);
|
||||
export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id");
|
||||
export const getUser = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?",
|
||||
);
|
||||
export const getAllUsers = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC",
|
||||
);
|
||||
export const getAllAkhys = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC",
|
||||
);
|
||||
|
||||
export const insertSkin = flopoDB.prepare('INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, basePrice, currentLvl, currentChroma, currentPrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)');
|
||||
export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid');
|
||||
export const hardUpdateSkin = flopoDB.prepare('UPDATE skins SET displayName = @displayName, contentTierUuid = @contentTierUuid, displayIcon = @displayIcon, tierRank = @tierRank, tierColor = @tierColor, tierText = @tierText, basePrice = @basePrice, user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice, maxPrice = @maxPrice WHERE uuid = @uuid');
|
||||
export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?');
|
||||
export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC');
|
||||
export const getAllAvailableSkins = flopoDB.prepare('SELECT * FROM skins WHERE user_id IS NULL');
|
||||
export const getUserInventory = flopoDB.prepare('SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC');
|
||||
export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10');
|
||||
export const insertSkin = flopoDB.prepare(
|
||||
`INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
|
||||
basePrice, currentLvl, currentChroma, currentPrice, maxPrice)
|
||||
VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
|
||||
@basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`,
|
||||
);
|
||||
export const updateSkin = flopoDB.prepare(
|
||||
`UPDATE skins
|
||||
SET user_id = @user_id,
|
||||
currentLvl = @currentLvl,
|
||||
currentChroma = @currentChroma,
|
||||
currentPrice = @currentPrice
|
||||
WHERE uuid = @uuid`,
|
||||
);
|
||||
export const hardUpdateSkin = flopoDB.prepare(
|
||||
`UPDATE skins
|
||||
SET displayName = @displayName,
|
||||
contentTierUuid = @contentTierUuid,
|
||||
displayIcon = @displayIcon,
|
||||
tierRank = @tierRank,
|
||||
tierColor = @tierColor,
|
||||
tierText = @tierText,
|
||||
basePrice = @basePrice,
|
||||
user_id = @user_id,
|
||||
currentLvl = @currentLvl,
|
||||
currentChroma = @currentChroma,
|
||||
currentPrice = @currentPrice,
|
||||
maxPrice = @maxPrice
|
||||
WHERE uuid = @uuid`,
|
||||
);
|
||||
export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?");
|
||||
export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC");
|
||||
export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL");
|
||||
export const getUserInventory = flopoDB.prepare(
|
||||
"SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC",
|
||||
);
|
||||
export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10");
|
||||
|
||||
export const stmtMarketOffers = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS market_offers
|
||||
(
|
||||
id PRIMARY KEY,
|
||||
skin_uuid TEXT REFERENCES skins,
|
||||
seller_id TEXT REFERENCES users,
|
||||
starting_price INTEGER NOT NULL,
|
||||
buyout_price INTEGER DEFAULT NULL,
|
||||
final_price INTEGER DEFAULT NULL,
|
||||
status TEXT NOT NULL,
|
||||
posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
opening_at TIMESTAMP NOT NULL,
|
||||
closing_at TIMESTAMP NOT NULL,
|
||||
buyer_id TEXT REFERENCES users DEFAULT NULL
|
||||
)
|
||||
`);
|
||||
stmtMarketOffers.run();
|
||||
|
||||
export const stmtBids = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS bids
|
||||
(
|
||||
id PRIMARY KEY,
|
||||
bidder_id TEXT REFERENCES users,
|
||||
market_offer_id REFERENCES market_offers,
|
||||
offer_amount INTEGER,
|
||||
offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtBids.run();
|
||||
|
||||
export const getMarketOffers = flopoDB.prepare(`
|
||||
SELECT market_offers.*,
|
||||
skins.displayName AS skinName,
|
||||
skins.displayIcon AS skinIcon,
|
||||
seller.username AS sellerName,
|
||||
seller.globalName AS sellerGlobalName,
|
||||
buyer.username AS buyerName,
|
||||
buyer.globalName AS buyerGlobalName
|
||||
FROM market_offers
|
||||
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||
ORDER BY market_offers.posted_at DESC
|
||||
`);
|
||||
|
||||
export const getMarketOfferById = flopoDB.prepare(`
|
||||
SELECT market_offers.*,
|
||||
skins.displayName AS skinName,
|
||||
skins.displayIcon AS skinIcon,
|
||||
seller.username AS sellerName,
|
||||
seller.globalName AS sellerGlobalName,
|
||||
buyer.username AS buyerName,
|
||||
buyer.globalName AS buyerGlobalName
|
||||
FROM market_offers
|
||||
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||
WHERE market_offers.id = ?
|
||||
`);
|
||||
|
||||
export const insertMarketOffer = flopoDB.prepare(`
|
||||
INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at)
|
||||
VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at)
|
||||
`);
|
||||
|
||||
export const getBids = flopoDB.prepare(`
|
||||
SELECT bids.*,
|
||||
bidder.username AS bidderName,
|
||||
bidder.globalName AS bidderGlobalName
|
||||
FROM bids
|
||||
JOIN users AS bidder ON bidder.id = bids.bidder_id
|
||||
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||
`);
|
||||
|
||||
export const getBidById = flopoDB.prepare(`
|
||||
SELECT bids.*
|
||||
FROM bids
|
||||
WHERE bids.id = ?
|
||||
`);
|
||||
|
||||
export const getOfferBids = flopoDB.prepare(`
|
||||
SELECT bids.*
|
||||
FROM bids
|
||||
WHERE bids.market_offer_id = ?
|
||||
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||
`);
|
||||
|
||||
export const insertBid = flopoDB.prepare(`
|
||||
INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount)
|
||||
VALUES (@id, @bidder_id, @market_offer_id, @offer_amount)
|
||||
`);
|
||||
|
||||
export const insertManyUsers = flopoDB.transaction(async (users) => {
|
||||
for (const user of users) try { await insertUser.run(user) } catch (e) { /**/ }
|
||||
for (const user of users)
|
||||
try {
|
||||
await insertUser.run(user);
|
||||
} catch (e) {}
|
||||
});
|
||||
export const updateManyUsers = flopoDB.transaction(async (users) => {
|
||||
for (const user of users) try { await updateUser.run(user) } catch (e) { console.log('user update failed') }
|
||||
for (const user of users)
|
||||
try {
|
||||
await updateUser.run(user);
|
||||
} catch (e) {
|
||||
console.log("user update failed");
|
||||
}
|
||||
});
|
||||
|
||||
export const insertManySkins = flopoDB.transaction(async (skins) => {
|
||||
for (const skin of skins) try { await insertSkin.run(skin) } catch (e) {}
|
||||
for (const skin of skins)
|
||||
try {
|
||||
await insertSkin.run(skin);
|
||||
} catch (e) {}
|
||||
});
|
||||
export const updateManySkins = flopoDB.transaction(async (skins) => {
|
||||
for (const skin of skins) try { await updateSkin.run(skin) } catch (e) {}
|
||||
for (const skin of skins)
|
||||
try {
|
||||
await updateSkin.run(skin);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
|
||||
export const stmtLogs = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users,
|
||||
action TEXT,
|
||||
target_user_id TEXT REFERENCES users,
|
||||
coins_amount INTEGER,
|
||||
user_new_amount INTEGER
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS logs
|
||||
(
|
||||
id PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users,
|
||||
action TEXT,
|
||||
target_user_id TEXT REFERENCES users,
|
||||
coins_amount INTEGER,
|
||||
user_new_amount INTEGER
|
||||
)
|
||||
`);
|
||||
stmtLogs.run()
|
||||
|
||||
export const insertLog = flopoDB.prepare('INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)');
|
||||
export const getLogs = flopoDB.prepare('SELECT * FROM logs');
|
||||
export const getUserLogs = flopoDB.prepare('SELECT * FROM logs WHERE user_id = @user_id');
|
||||
stmtLogs.run();
|
||||
|
||||
export const insertLog = flopoDB.prepare(
|
||||
`INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount)
|
||||
VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`,
|
||||
);
|
||||
export const getLogs = flopoDB.prepare("SELECT * FROM logs");
|
||||
export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id");
|
||||
|
||||
export const stmtGames = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id PRIMARY KEY,
|
||||
p1 TEXT REFERENCES users,
|
||||
p2 TEXT REFERENCES users,
|
||||
p1_score INTEGER,
|
||||
p2_score INTEGER,
|
||||
p1_elo INTEGER,
|
||||
p2_elo INTEGER,
|
||||
p1_new_elo INTEGER,
|
||||
p2_new_elo INTEGER,
|
||||
type TEXT,
|
||||
timestamp TIMESTAMP
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS games
|
||||
(
|
||||
id PRIMARY KEY,
|
||||
p1 TEXT REFERENCES users,
|
||||
p2 TEXT REFERENCES users,
|
||||
p1_score INTEGER,
|
||||
p2_score INTEGER,
|
||||
p1_elo INTEGER,
|
||||
p2_elo INTEGER,
|
||||
p1_new_elo INTEGER,
|
||||
p2_new_elo INTEGER,
|
||||
type TEXT,
|
||||
timestamp TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtGames.run()
|
||||
|
||||
export const insertGame = flopoDB.prepare('INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)');
|
||||
export const getGames = flopoDB.prepare('SELECT * FROM games');
|
||||
export const getUserGames = flopoDB.prepare('SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp');
|
||||
stmtGames.run();
|
||||
|
||||
export const insertGame = flopoDB.prepare(
|
||||
`INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp)
|
||||
VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`,
|
||||
);
|
||||
export const getGames = flopoDB.prepare("SELECT * FROM games");
|
||||
export const getUserGames = flopoDB.prepare(
|
||||
"SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp",
|
||||
);
|
||||
|
||||
export const stmtElos = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS elos (
|
||||
id PRIMARY KEY REFERENCES users,
|
||||
elo INTEGER
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS elos
|
||||
(
|
||||
id PRIMARY KEY REFERENCES users,
|
||||
elo INTEGER
|
||||
)
|
||||
`);
|
||||
stmtElos.run()
|
||||
stmtElos.run();
|
||||
|
||||
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) VALUES (@id, @elo)`);
|
||||
export const getElos = flopoDB.prepare(`SELECT * FROM elos`);
|
||||
export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`);
|
||||
export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id');
|
||||
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
|
||||
VALUES (@id, @elo)`);
|
||||
export const getElos = flopoDB.prepare(`SELECT *
|
||||
FROM elos`);
|
||||
export const getUserElo = flopoDB.prepare(`SELECT *
|
||||
FROM elos
|
||||
WHERE id = @id`);
|
||||
export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id");
|
||||
|
||||
|
||||
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC')
|
||||
export const getUsersByElo = flopoDB.prepare(
|
||||
"SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
|
||||
);
|
||||
|
||||
export const stmtSOTD = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS sotd (
|
||||
id INT PRIMARY KEY,
|
||||
tableauPiles TEXT,
|
||||
foundationPiles TEXT,
|
||||
stockPile TEXT,
|
||||
wastePile TEXT,
|
||||
isDone BOOLEAN DEFAULT false,
|
||||
seed TEXT
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS sotd
|
||||
(
|
||||
id INT PRIMARY KEY,
|
||||
tableauPiles TEXT,
|
||||
foundationPiles TEXT,
|
||||
stockPile TEXT,
|
||||
wastePile TEXT,
|
||||
isDone BOOLEAN DEFAULT false,
|
||||
seed TEXT
|
||||
)
|
||||
`);
|
||||
stmtSOTD.run()
|
||||
stmtSOTD.run();
|
||||
|
||||
export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`)
|
||||
export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`)
|
||||
export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`)
|
||||
export const getSOTD = flopoDB.prepare(`SELECT *
|
||||
FROM sotd
|
||||
WHERE id = '0'`);
|
||||
export const insertSOTD =
|
||||
flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed)
|
||||
VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`);
|
||||
export const deleteSOTD = flopoDB.prepare(`DELETE
|
||||
FROM sotd
|
||||
WHERE id = '0'`);
|
||||
|
||||
export const stmtSOTDStats = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS sotd_stats (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users,
|
||||
time INTEGER,
|
||||
moves INTEGER,
|
||||
score INTEGER
|
||||
)
|
||||
CREATE TABLE IF NOT EXISTS sotd_stats
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users,
|
||||
time INTEGER,
|
||||
moves 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 getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`);
|
||||
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`);
|
||||
export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`);
|
||||
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`);
|
||||
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
|
||||
FROM sotd_stats
|
||||
JOIN users ON users.id = sotd_stats.user_id
|
||||
ORDER BY score DESC, moves ASC, time ASC`);
|
||||
export const getUserSOTDStats = flopoDB.prepare(`SELECT *
|
||||
FROM sotd_stats
|
||||
WHERE user_id = ?`);
|
||||
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score)
|
||||
VALUES (@id, @user_id, @time, @moves, @score)`);
|
||||
export const clearSOTDStats = flopoDB.prepare(`DELETE
|
||||
FROM sotd_stats`);
|
||||
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE
|
||||
FROM sotd_stats
|
||||
WHERE user_id = ?`);
|
||||
|
||||
export async function pruneOldLogs() {
|
||||
const users = flopoDB.prepare(`
|
||||
SELECT user_id
|
||||
FROM logs
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
||||
`).all();
|
||||
const users = flopoDB
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id
|
||||
FROM logs
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
|
||||
const transaction = flopoDB.transaction(() => {
|
||||
for (const { user_id } of users) {
|
||||
flopoDB.prepare(`
|
||||
DELETE FROM logs
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
|
||||
FROM logs
|
||||
WHERE user_id = ?
|
||||
)
|
||||
WHERE rn > ${process.env.LOGS_BY_USER}
|
||||
)
|
||||
`).run(user_id);
|
||||
}
|
||||
});
|
||||
const transaction = flopoDB.transaction(() => {
|
||||
for (const { user_id } of users) {
|
||||
flopoDB
|
||||
.prepare(
|
||||
`
|
||||
DELETE
|
||||
FROM logs
|
||||
WHERE id IN (SELECT id
|
||||
FROM (SELECT id,
|
||||
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
|
||||
FROM logs
|
||||
WHERE user_id = ?)
|
||||
WHERE rn > ${process.env.LOGS_BY_USER})
|
||||
`,
|
||||
)
|
||||
.run(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
transaction()
|
||||
transaction();
|
||||
}
|
||||
@@ -2,384 +2,425 @@
|
||||
// Core blackjack helpers for a single continuous room.
|
||||
// Inspired by your poker helpers API style.
|
||||
|
||||
import {emitToast} from "../server/socket.js";
|
||||
import {getUser, insertLog, updateUserCoins} from "../database/index.js";
|
||||
import {client} from "../bot/client.js";
|
||||
import {EmbedBuilder} from "discord.js";
|
||||
import { emitToast } from "../server/socket.js";
|
||||
import { getUser, insertLog, updateUserCoins } from "../database/index.js";
|
||||
import { client } from "../bot/client.js";
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
|
||||
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 RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
|
||||
export const SUITS = ["d", "s", "c", "h"];
|
||||
|
||||
// Build a single 52-card deck like "Ad","Ts", etc.
|
||||
export const singleDeck = RANKS.flatMap(r => SUITS.map(s => `${r}${s}`));
|
||||
export const singleDeck = RANKS.flatMap((r) => SUITS.map((s) => `${r}${s}`));
|
||||
|
||||
export function buildShoe(decks = 6) {
|
||||
const shoe = [];
|
||||
for (let i = 0; i < decks; i++) shoe.push(...singleDeck);
|
||||
return shuffle(shoe);
|
||||
const shoe = [];
|
||||
for (let i = 0; i < decks; i++) shoe.push(...singleDeck);
|
||||
return shuffle(shoe);
|
||||
}
|
||||
|
||||
export function shuffle(arr) {
|
||||
// Fisher–Yates
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
// Fisher–Yates
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// Draw one card from the shoe; if empty, caller should reshuffle at end of round.
|
||||
export function draw(shoe) {
|
||||
return shoe.pop();
|
||||
return shoe.pop();
|
||||
}
|
||||
|
||||
// Return an object describing the best value of a hand with flexible Aces.
|
||||
export function handValue(cards) {
|
||||
// Count with all aces as 11, then reduce as needed
|
||||
let total = 0;
|
||||
let aces = 0;
|
||||
for (const c of cards) {
|
||||
const r = c[0];
|
||||
if (r === "A") { total += 11; aces += 1; }
|
||||
else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
|
||||
else total += Number(r);
|
||||
}
|
||||
while (total > 21 && aces > 0) {
|
||||
total -= 10; // convert an Ace from 11 to 1
|
||||
aces -= 1;
|
||||
}
|
||||
const soft = (aces > 0); // if any Ace still counted as 11, it's a soft hand
|
||||
return { total, soft };
|
||||
// Count with all aces as 11, then reduce as needed
|
||||
let total = 0;
|
||||
let aces = 0;
|
||||
for (const c of cards) {
|
||||
const r = c[0];
|
||||
if (r === "A") {
|
||||
total += 11;
|
||||
aces += 1;
|
||||
} else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
|
||||
else total += Number(r);
|
||||
}
|
||||
while (total > 21 && aces > 0) {
|
||||
total -= 10; // convert an Ace from 11 to 1
|
||||
aces -= 1;
|
||||
}
|
||||
const soft = aces > 0; // if any Ace still counted as 11, it's a soft hand
|
||||
return { total, soft };
|
||||
}
|
||||
|
||||
export function isBlackjack(cards) {
|
||||
return cards.length === 2 && handValue(cards).total === 21;
|
||||
return cards.length === 2 && handValue(cards).total === 21;
|
||||
}
|
||||
|
||||
export function isBust(cards) {
|
||||
return handValue(cards).total > 21;
|
||||
return handValue(cards).total > 21;
|
||||
}
|
||||
|
||||
// Dealer draw rule. By default, dealer stands on soft 17 (S17).
|
||||
export function dealerShouldHit(dealerCards, hitSoft17 = false) {
|
||||
const v = handValue(dealerCards);
|
||||
if (v.total < 17) return true;
|
||||
if (v.total === 17 && v.soft && hitSoft17) return true;
|
||||
return false;
|
||||
const v = handValue(dealerCards);
|
||||
if (v.total < 17) return true;
|
||||
if (v.total === 17 && v.soft && hitSoft17) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare a player hand to dealer and return outcome.
|
||||
export function compareHands(playerCards, dealerCards) {
|
||||
const pv = handValue(playerCards).total;
|
||||
const dv = handValue(dealerCards).total;
|
||||
if (pv > 21) return "lose";
|
||||
if (dv > 21) return "win";
|
||||
if (pv > dv) return "win";
|
||||
if (pv < dv) return "lose";
|
||||
return "push";
|
||||
const pv = handValue(playerCards).total;
|
||||
const dv = handValue(dealerCards).total;
|
||||
if (pv > 21) return "lose";
|
||||
if (dv > 21) return "win";
|
||||
if (pv > dv) return "win";
|
||||
if (pv < dv) return "lose";
|
||||
return "push";
|
||||
}
|
||||
|
||||
// Compute payout for a single finished hand (no splits here).
|
||||
// options: { blackjackPayout: 1.5, allowSurrender: false }
|
||||
export function settleHand({ bet, playerCards, dealerCards, doubled = false, surrendered = false, blackjackPayout = 1.5 }) {
|
||||
if (surrendered) return { delta: -bet / 2, result: "surrender" };
|
||||
export function settleHand({
|
||||
bet,
|
||||
playerCards,
|
||||
dealerCards,
|
||||
doubled = false,
|
||||
surrendered = false,
|
||||
blackjackPayout = 1.5,
|
||||
}) {
|
||||
if (surrendered) return { delta: -bet / 2, result: "surrender" };
|
||||
|
||||
const pBJ = isBlackjack(playerCards);
|
||||
const dBJ = isBlackjack(dealerCards);
|
||||
const pBJ = isBlackjack(playerCards);
|
||||
const dBJ = isBlackjack(dealerCards);
|
||||
|
||||
if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" };
|
||||
if (!pBJ && dBJ) return { delta: -bet, result: "lose" };
|
||||
if (pBJ && dBJ) return { delta: 0, result: "push" };
|
||||
if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" };
|
||||
if (!pBJ && dBJ) return { delta: -bet, result: "lose" };
|
||||
if (pBJ && dBJ) return { delta: 0, result: "push" };
|
||||
|
||||
const outcome = compareHands(playerCards, dealerCards);
|
||||
let unit = bet;
|
||||
if (outcome === "win") return { delta: unit, result: "win" };
|
||||
if (outcome === "lose") return { delta: -unit, result: "lose" };
|
||||
return { delta: 0, result: "push" };
|
||||
const outcome = compareHands(playerCards, dealerCards);
|
||||
let unit = bet;
|
||||
if (outcome === "win") return { delta: unit, result: "win" };
|
||||
if (outcome === "lose") return { delta: -unit, result: "lose" };
|
||||
return { delta: 0, result: "push" };
|
||||
}
|
||||
|
||||
// Helper to decide if doubling is still allowed (first decision, 2 cards, not hit yet).
|
||||
export function canDouble(hand) {
|
||||
return hand.cards.length === 2 && !hand.hasActed;
|
||||
return hand.cards.length === 2 && !hand.hasActed;
|
||||
}
|
||||
|
||||
// Very small utility to format a public-safe snapshot of room state
|
||||
export function publicPlayerView(player) {
|
||||
// Hide hole cards until dealer reveal is fine for dealer only; player cards are visible.
|
||||
return {
|
||||
id: player.id,
|
||||
globalName: player.globalName,
|
||||
avatar: player.avatar,
|
||||
bank: player.bank,
|
||||
currentBet: player.currentBet,
|
||||
inRound: player.inRound,
|
||||
hands: player.hands.map(h => ({
|
||||
cards: h.cards,
|
||||
stood: h.stood,
|
||||
busted: h.busted,
|
||||
doubled: h.doubled,
|
||||
surrendered: h.surrendered,
|
||||
result: h.result ?? null,
|
||||
total: handValue(h.cards).total,
|
||||
soft: handValue(h.cards).soft,
|
||||
bet: h.bet,
|
||||
})),
|
||||
};
|
||||
// Hide hole cards until dealer reveal is fine for dealer only; player cards are visible.
|
||||
return {
|
||||
id: player.id,
|
||||
globalName: player.globalName,
|
||||
avatar: player.avatar,
|
||||
bank: player.bank,
|
||||
currentBet: player.currentBet,
|
||||
inRound: player.inRound,
|
||||
hands: player.hands.map((h) => ({
|
||||
cards: h.cards,
|
||||
stood: h.stood,
|
||||
busted: h.busted,
|
||||
doubled: h.doubled,
|
||||
surrendered: h.surrendered,
|
||||
result: h.result ?? null,
|
||||
total: handValue(h.cards).total,
|
||||
soft: handValue(h.cards).soft,
|
||||
bet: h.bet,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Build initial room object
|
||||
export function createBlackjackRoom({
|
||||
minBet = 10,
|
||||
maxBet = 10000,
|
||||
fakeMoney = false,
|
||||
decks = 6,
|
||||
hitSoft17 = false,
|
||||
blackjackPayout = 1.5,
|
||||
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
|
||||
phaseDurations = {
|
||||
bettingMs: 15000,
|
||||
dealMs: 1000,
|
||||
playMsPerPlayer: 20000,
|
||||
revealMs: 1000,
|
||||
payoutMs: 10000,
|
||||
},
|
||||
animation = {
|
||||
dealerDrawMs: 500,
|
||||
}
|
||||
minBet = 10,
|
||||
maxBet = 10000,
|
||||
fakeMoney = false,
|
||||
decks = 6,
|
||||
hitSoft17 = false,
|
||||
blackjackPayout = 1.5,
|
||||
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
|
||||
phaseDurations = {
|
||||
bettingMs: 15000,
|
||||
dealMs: 1000,
|
||||
playMsPerPlayer: 20000,
|
||||
revealMs: 1000,
|
||||
payoutMs: 10000,
|
||||
},
|
||||
animation = {
|
||||
dealerDrawMs: 500,
|
||||
},
|
||||
} = {}) {
|
||||
return {
|
||||
id: "blackjack-room",
|
||||
name: "Blackjack",
|
||||
created_at: Date.now(),
|
||||
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
|
||||
phase_ends_at: Date.now() + phaseDurations.bettingMs,
|
||||
minBet, maxBet, fakeMoney,
|
||||
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation },
|
||||
shoe: buildShoe(decks),
|
||||
discard: [],
|
||||
dealer: { cards: [], holeHidden: true },
|
||||
players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 }
|
||||
leavingAfterRound: {},
|
||||
};
|
||||
return {
|
||||
id: "blackjack-room",
|
||||
name: "Blackjack",
|
||||
created_at: Date.now(),
|
||||
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
|
||||
phase_ends_at: Date.now() + phaseDurations.bettingMs,
|
||||
minBet,
|
||||
maxBet,
|
||||
fakeMoney,
|
||||
settings: {
|
||||
decks,
|
||||
hitSoft17,
|
||||
blackjackPayout,
|
||||
cutCardRatio,
|
||||
phaseDurations,
|
||||
animation,
|
||||
},
|
||||
shoe: buildShoe(decks),
|
||||
discard: [],
|
||||
dealer: { cards: [], holeHidden: true },
|
||||
players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 }
|
||||
leavingAfterRound: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Reshuffle at start of the next round if the shoe is low
|
||||
export function needsReshuffle(room) {
|
||||
return room.shoe.length < singleDeck.length * room.settings.decks * room.settings.cutCardRatio;
|
||||
return room.shoe.length < singleDeck.length * room.settings.decks * room.settings.cutCardRatio;
|
||||
}
|
||||
|
||||
// --- Round Lifecycle helpers ---
|
||||
|
||||
export function resetForNewRound(room) {
|
||||
room.status = "betting";
|
||||
room.dealer = { cards: [], holeHidden: true };
|
||||
room.leavingAfterRound = {};
|
||||
// Clear per-round attributes on players, but keep bank and presence
|
||||
for (const p of Object.values(room.players)) {
|
||||
p.inRound = false;
|
||||
p.currentBet = 0;
|
||||
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 } ];
|
||||
p.activeHand = 0;
|
||||
}
|
||||
room.status = "betting";
|
||||
room.dealer = { cards: [], holeHidden: true };
|
||||
room.leavingAfterRound = {};
|
||||
// Clear per-round attributes on players, but keep bank and presence
|
||||
for (const p of Object.values(room.players)) {
|
||||
p.inRound = false;
|
||||
p.currentBet = 0;
|
||||
p.hands = [
|
||||
{
|
||||
cards: [],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
bet: 0,
|
||||
},
|
||||
];
|
||||
p.activeHand = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function startBetting(room, now) {
|
||||
resetForNewRound(room);
|
||||
if (needsReshuffle(room)) {
|
||||
room.status = "shuffle";
|
||||
// quick shuffle animation phase
|
||||
room.shoe = buildShoe(room.settings.decks);
|
||||
}
|
||||
room.status = "betting";
|
||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||
resetForNewRound(room);
|
||||
if (needsReshuffle(room)) {
|
||||
room.status = "shuffle";
|
||||
// quick shuffle animation phase
|
||||
room.shoe = buildShoe(room.settings.decks);
|
||||
}
|
||||
room.status = "betting";
|
||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||
}
|
||||
|
||||
export function dealInitial(room) {
|
||||
room.status = "dealing";
|
||||
// Deal one to each player who placed a bet, then again, then dealer up + hole
|
||||
const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet);
|
||||
for (const p of actives) {
|
||||
p.inRound = true;
|
||||
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: p.currentBet } ];
|
||||
}
|
||||
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
|
||||
room.dealer.holeHidden = true;
|
||||
for (const p of actives) {
|
||||
p.hands[0].cards.push(draw(room.shoe));
|
||||
}
|
||||
room.status = "playing";
|
||||
room.status = "dealing";
|
||||
// Deal one to each player who placed a bet, then again, then dealer up + hole
|
||||
const actives = Object.values(room.players).filter((p) => p.currentBet >= room.minBet);
|
||||
for (const p of actives) {
|
||||
p.inRound = true;
|
||||
p.hands = [
|
||||
{
|
||||
cards: [draw(room.shoe)],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
bet: p.currentBet,
|
||||
},
|
||||
];
|
||||
}
|
||||
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
|
||||
room.dealer.holeHidden = true;
|
||||
for (const p of actives) {
|
||||
p.hands[0].cards.push(draw(room.shoe));
|
||||
}
|
||||
room.status = "playing";
|
||||
}
|
||||
|
||||
export function autoActions(room) {
|
||||
// Auto-stand if player already blackjack
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (isBlackjack(h.cards)) {
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
}
|
||||
}
|
||||
// Auto-stand if player already blackjack
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (isBlackjack(h.cards)) {
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function everyoneDone(room) {
|
||||
return Object.values(room.players).every(p => {
|
||||
if (!p.inRound) return true;
|
||||
return p.hands.filter(h => !h.stood && !h.busted && !h.surrendered)?.length === 0;
|
||||
});
|
||||
return Object.values(room.players).every((p) => {
|
||||
if (!p.inRound) return true;
|
||||
return p.hands.filter((h) => !h.stood && !h.busted && !h.surrendered)?.length === 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function dealerPlay(room) {
|
||||
room.status = "dealer";
|
||||
room.dealer.holeHidden = false;
|
||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||
room.dealer.cards.push(draw(room.shoe));
|
||||
}
|
||||
room.status = "dealer";
|
||||
room.dealer.holeHidden = false;
|
||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||
room.dealer.cards.push(draw(room.shoe));
|
||||
}
|
||||
}
|
||||
|
||||
export async function settleAll(room) {
|
||||
room.status = "payout";
|
||||
const allRes = {}
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
for (const hand of p.hands) {
|
||||
const res = settleHand({
|
||||
bet: hand.bet,
|
||||
playerCards: hand.cards,
|
||||
dealerCards: room.dealer.cards,
|
||||
doubled: hand.doubled,
|
||||
surrendered: hand.surrendered,
|
||||
blackjackPayout: room.settings.blackjackPayout,
|
||||
});
|
||||
if (allRes[p.id]) {
|
||||
allRes[p.id].push(res);
|
||||
} else {
|
||||
allRes[p.id] = [res];
|
||||
}
|
||||
room.status = "payout";
|
||||
const allRes = {};
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
for (const hand of p.hands) {
|
||||
const res = settleHand({
|
||||
bet: hand.bet,
|
||||
playerCards: hand.cards,
|
||||
dealerCards: room.dealer.cards,
|
||||
doubled: hand.doubled,
|
||||
surrendered: hand.surrendered,
|
||||
blackjackPayout: room.settings.blackjackPayout,
|
||||
});
|
||||
if (allRes[p.id]) {
|
||||
allRes[p.id].push(res);
|
||||
} else {
|
||||
allRes[p.id] = [res];
|
||||
}
|
||||
|
||||
p.totalDelta += res.delta
|
||||
p.totalBets++
|
||||
if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
const coins = userDB.coins;
|
||||
try {
|
||||
updateUserCoins.run({ id: p.id, coins: coins + hand.bet + res.delta });
|
||||
insertLog.run({
|
||||
id: `${p.id}-blackjack-${Date.now()}`,
|
||||
user_id: p.id, target_user_id: null,
|
||||
action: 'BLACKJACK_PAYOUT',
|
||||
coins_amount: res.delta + hand.bet, user_new_amount: coins + hand.bet + res.delta,
|
||||
});
|
||||
p.bank = coins + hand.bet + res.delta
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
emitToast({ type: `payout-res`, allRes });
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${p.totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
p.totalDelta += res.delta;
|
||||
p.totalBets++;
|
||||
if (res.result === "win" || res.result === "push" || res.result === "blackjack") {
|
||||
const userDB = getUser.get(p.id);
|
||||
if (userDB) {
|
||||
const coins = userDB.coins;
|
||||
try {
|
||||
updateUserCoins.run({
|
||||
id: p.id,
|
||||
coins: coins + hand.bet + res.delta,
|
||||
});
|
||||
insertLog.run({
|
||||
id: `${p.id}-blackjack-${Date.now()}`,
|
||||
user_id: p.id,
|
||||
target_user_id: null,
|
||||
action: "BLACKJACK_PAYOUT",
|
||||
coins_amount: res.delta + hand.bet,
|
||||
user_new_amount: coins + hand.bet + res.delta,
|
||||
});
|
||||
p.bank = coins + hand.bet + res.delta;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
emitToast({ type: `payout-res`, allRes });
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${p.totalDelta >= 0 ? "+" + p.totalDelta : p.totalDelta}** Flopos`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${p.totalBets}**`,
|
||||
inline: true,
|
||||
},
|
||||
)
|
||||
.setColor(p.totalDelta >= 0 ? 0x22a55b : 0xed4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a player decision; returns a string event or throws on invalid.
|
||||
export function applyAction(room, playerId, action) {
|
||||
const p = room.players[playerId];
|
||||
if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed");
|
||||
const hand = p.hands[p.activeHand];
|
||||
const p = room.players[playerId];
|
||||
if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed");
|
||||
const hand = p.hands[p.activeHand];
|
||||
|
||||
switch (action) {
|
||||
case "hit": {
|
||||
if (hand.stood || hand.busted) throw new Error("Already ended");
|
||||
hand.hasActed = true;
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
return "hit";
|
||||
}
|
||||
case "stand": {
|
||||
hand.stood = true;
|
||||
hand.hasActed = true;
|
||||
p.activeHand++;
|
||||
return "stand";
|
||||
}
|
||||
case "double": {
|
||||
if (!canDouble(hand)) throw new Error("Cannot double now");
|
||||
hand.doubled = true;
|
||||
hand.bet*=2
|
||||
p.currentBet+=hand.bet/2
|
||||
hand.hasActed = true;
|
||||
// The caller (routes) must also handle additional balance lock on the bet if using real coins
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
else hand.stood = true;
|
||||
p.activeHand++;
|
||||
return "double";
|
||||
}
|
||||
case "split": {
|
||||
if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards");
|
||||
const r0 = hand.cards[0][0];
|
||||
const r1 = hand.cards[1][0];
|
||||
if (r0 !== r1) throw new Error("Cannot split: cards not same rank");
|
||||
switch (action) {
|
||||
case "hit": {
|
||||
if (hand.stood || hand.busted) throw new Error("Already ended");
|
||||
hand.hasActed = true;
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
return "hit";
|
||||
}
|
||||
case "stand": {
|
||||
hand.stood = true;
|
||||
hand.hasActed = true;
|
||||
p.activeHand++;
|
||||
return "stand";
|
||||
}
|
||||
case "double": {
|
||||
if (!canDouble(hand)) throw new Error("Cannot double now");
|
||||
hand.doubled = true;
|
||||
hand.bet *= 2;
|
||||
p.currentBet += hand.bet / 2;
|
||||
hand.hasActed = true;
|
||||
// The caller (routes) must also handle additional balance lock on the bet if using real coins
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
else hand.stood = true;
|
||||
p.activeHand++;
|
||||
return "double";
|
||||
}
|
||||
case "split": {
|
||||
if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards");
|
||||
const r0 = hand.cards[0][0];
|
||||
const r1 = hand.cards[1][0];
|
||||
if (r0 !== r1) throw new Error("Cannot split: cards not same rank");
|
||||
|
||||
const cardA = hand.cards[0];
|
||||
const cardB = hand.cards[1];
|
||||
const cardA = hand.cards[0];
|
||||
const cardB = hand.cards[1];
|
||||
|
||||
hand.cards = [cardA];
|
||||
hand.stood = false;
|
||||
hand.busted = false;
|
||||
hand.doubled = false;
|
||||
hand.surrendered = false;
|
||||
hand.hasActed = false;
|
||||
hand.cards = [cardA];
|
||||
hand.stood = false;
|
||||
hand.busted = false;
|
||||
hand.doubled = false;
|
||||
hand.surrendered = false;
|
||||
hand.hasActed = false;
|
||||
|
||||
const newHand = {
|
||||
cards: [cardB],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
bet: hand.bet,
|
||||
}
|
||||
const newHand = {
|
||||
cards: [cardB],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
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);
|
||||
|
||||
hand.cards.push(draw(room.shoe));
|
||||
newHand.cards.push(draw(room.shoe));
|
||||
hand.cards.push(draw(room.shoe));
|
||||
newHand.cards.push(draw(room.shoe));
|
||||
|
||||
return "split";
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid action");
|
||||
}
|
||||
return "split";
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid action");
|
||||
}
|
||||
}
|
||||
250
src/game/elo.js
250
src/game/elo.js
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
getUser,
|
||||
getUserElo,
|
||||
insertElos,
|
||||
updateElo,
|
||||
insertGame,
|
||||
} from '../database/index.js';
|
||||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
||||
import {client} from "../bot/client.js";
|
||||
import { getUser, getUserElo, insertElos, updateElo, insertGame } from "../database/index.js";
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||
import { client } from "../bot/client.js";
|
||||
|
||||
/**
|
||||
* Handles Elo calculation for a standard 1v1 game.
|
||||
@@ -17,81 +11,85 @@ import {client} from "../bot/client.js";
|
||||
* @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4').
|
||||
*/
|
||||
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
// --- 1. Fetch Player Data ---
|
||||
const p1DB = getUser.get(p1Id);
|
||||
const p2DB = getUser.get(p2Id);
|
||||
if (!p1DB || !p2DB) {
|
||||
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
|
||||
return;
|
||||
}
|
||||
// --- 1. Fetch Player Data ---
|
||||
const p1DB = getUser.get(p1Id);
|
||||
const p2DB = getUser.get(p2Id);
|
||||
if (!p1DB || !p2DB) {
|
||||
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let p1EloData = getUserElo.get({ id: p1Id });
|
||||
let p2EloData = getUserElo.get({ id: p2Id });
|
||||
let p1EloData = getUserElo.get({ id: p1Id });
|
||||
let p2EloData = getUserElo.get({ id: p2Id });
|
||||
|
||||
// --- 2. Initialize Elo if it doesn't exist ---
|
||||
if (!p1EloData) {
|
||||
await insertElos.run({ id: p1Id, elo: 1000 });
|
||||
p1EloData = { id: p1Id, elo: 1000 };
|
||||
}
|
||||
if (!p2EloData) {
|
||||
await insertElos.run({ id: p2Id, elo: 1000 });
|
||||
p2EloData = { id: p2Id, elo: 1000 };
|
||||
}
|
||||
// --- 2. Initialize Elo if it doesn't exist ---
|
||||
if (!p1EloData) {
|
||||
await insertElos.run({ id: p1Id, elo: 1000 });
|
||||
p1EloData = { id: p1Id, elo: 1000 };
|
||||
}
|
||||
if (!p2EloData) {
|
||||
await insertElos.run({ id: p2Id, elo: 1000 });
|
||||
p2EloData = { id: p2Id, elo: 1000 };
|
||||
}
|
||||
|
||||
const p1CurrentElo = p1EloData.elo;
|
||||
const p2CurrentElo = p2EloData.elo;
|
||||
const p1CurrentElo = p1EloData.elo;
|
||||
const p2CurrentElo = p2EloData.elo;
|
||||
|
||||
// --- 3. Calculate Elo Change ---
|
||||
// The K-factor determines how much the Elo rating changes after a game.
|
||||
const K_FACTOR = 32;
|
||||
// --- 3. Calculate Elo Change ---
|
||||
// The K-factor determines how much the Elo rating changes after a game.
|
||||
const K_FACTOR = 32;
|
||||
|
||||
// Calculate expected scores
|
||||
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
|
||||
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
|
||||
// Calculate expected scores
|
||||
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
|
||||
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
|
||||
|
||||
// Calculate new Elo ratings
|
||||
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
|
||||
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
|
||||
// Calculate new Elo ratings
|
||||
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
|
||||
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
|
||||
|
||||
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
|
||||
const finalP1Elo = Math.max(0, p1NewElo);
|
||||
const finalP2Elo = Math.max(0, p2NewElo);
|
||||
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
|
||||
const finalP1Elo = Math.max(0, p1NewElo);
|
||||
const finalP2Elo = Math.max(0, p2NewElo);
|
||||
|
||||
console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`);
|
||||
console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user1 = await client.users.fetch(p1Id);
|
||||
const user2 = await client.users.fetch(p2Id);
|
||||
const diff1 = finalP1Elo - p1CurrentElo;
|
||||
const diff2 = finalP2Elo - p2CurrentElo;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`FlopoRank - ${type}`)
|
||||
.setDescription(`
|
||||
**${user1.globalName || user1.username}** a ${diff1 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? '↗️' : '↘️'} **${finalP1Elo}**\n
|
||||
**${user2.globalName || user2.username}** a ${diff2 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? '↗️' : '↘️'} **${finalP2Elo}**\n
|
||||
`)
|
||||
.setColor('#5865f2');
|
||||
await generalChannel.send({ embeds: [embed] });
|
||||
} catch (e) { console.error(`Failed to post elo update message`, e); }
|
||||
console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`);
|
||||
console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user1 = await client.users.fetch(p1Id);
|
||||
const user2 = await client.users.fetch(p2Id);
|
||||
const diff1 = finalP1Elo - p1CurrentElo;
|
||||
const diff2 = finalP2Elo - p2CurrentElo;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`FlopoRank - ${type}`)
|
||||
.setDescription(
|
||||
`
|
||||
**${user1.globalName || user1.username}** a ${diff1 > 0 ? "gagné" : "perdu"} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? "↗️" : "↘️"} **${finalP1Elo}**\n
|
||||
**${user2.globalName || user2.username}** a ${diff2 > 0 ? "gagné" : "perdu"} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? "↗️" : "↘️"} **${finalP2Elo}**\n
|
||||
`,
|
||||
)
|
||||
.setColor("#5865f2");
|
||||
await generalChannel.send({ embeds: [embed] });
|
||||
} catch (e) {
|
||||
console.error(`Failed to post elo update message`, e);
|
||||
}
|
||||
|
||||
// --- 4. Update Database ---
|
||||
updateElo.run({ id: p1Id, elo: finalP1Elo });
|
||||
updateElo.run({ id: p2Id, elo: finalP2Elo });
|
||||
// --- 4. Update Database ---
|
||||
updateElo.run({ id: p1Id, elo: finalP1Elo });
|
||||
updateElo.run({ id: p2Id, elo: finalP2Elo });
|
||||
|
||||
insertGame.run({
|
||||
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
||||
p1: p1Id,
|
||||
p2: p2Id,
|
||||
p1_score: p1Score,
|
||||
p2_score: p2Score,
|
||||
p1_elo: p1CurrentElo,
|
||||
p2_elo: p2CurrentElo,
|
||||
p1_new_elo: finalP1Elo,
|
||||
p2_new_elo: finalP2Elo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
insertGame.run({
|
||||
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
||||
p1: p1Id,
|
||||
p2: p2Id,
|
||||
p1_score: p1Score,
|
||||
p2_score: p2Score,
|
||||
p1_elo: p1CurrentElo,
|
||||
p2_elo: p2CurrentElo,
|
||||
p1_new_elo: finalP1Elo,
|
||||
p2_new_elo: finalP2Elo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,64 +97,66 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
* @param {object} room - The poker room object containing player and winner info.
|
||||
*/
|
||||
export async function pokerEloHandler(room) {
|
||||
if (room.fakeMoney) {
|
||||
console.log("Skipping Elo update for fake money poker game.");
|
||||
return;
|
||||
}
|
||||
if (room.fakeMoney) {
|
||||
console.log("Skipping Elo update for fake money poker game.");
|
||||
return;
|
||||
}
|
||||
|
||||
const playerIds = Object.keys(room.players);
|
||||
if (playerIds.length < 2) return; // Not enough players to calculate Elo
|
||||
const playerIds = Object.keys(room.players);
|
||||
if (playerIds.length < 2) return; // Not enough players to calculate Elo
|
||||
|
||||
// Fetch all players' Elo data at once
|
||||
const dbPlayers = playerIds.map(id => {
|
||||
const user = getUser.get(id);
|
||||
const elo = getUserElo.get({ id })?.elo || 1000;
|
||||
return { ...user, elo };
|
||||
});
|
||||
// Fetch all players' Elo data at once
|
||||
const dbPlayers = playerIds.map((id) => {
|
||||
const user = getUser.get(id);
|
||||
const elo = getUserElo.get({ id })?.elo || 1000;
|
||||
return { ...user, elo };
|
||||
});
|
||||
|
||||
const winnerIds = new Set(room.winners);
|
||||
const playerCount = dbPlayers.length;
|
||||
const K_BASE = 16; // A lower K-factor is often used for multi-player games
|
||||
const winnerIds = new Set(room.winners);
|
||||
const playerCount = dbPlayers.length;
|
||||
const K_BASE = 16; // A lower K-factor is often used for multi-player games
|
||||
|
||||
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 => {
|
||||
// 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));
|
||||
dbPlayers.forEach((player) => {
|
||||
// Expected score is the chance of winning against an "average" player from the field
|
||||
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
|
||||
|
||||
// Determine actual score
|
||||
let actualScore;
|
||||
if (winnerIds.has(player.id)) {
|
||||
// Winners share the "win" points
|
||||
actualScore = 1 / winnerIds.size;
|
||||
} else {
|
||||
actualScore = 0;
|
||||
}
|
||||
// Determine actual score
|
||||
let actualScore;
|
||||
if (winnerIds.has(player.id)) {
|
||||
// Winners share the "win" points
|
||||
actualScore = 1 / winnerIds.size;
|
||||
} else {
|
||||
actualScore = 0;
|
||||
}
|
||||
|
||||
// Dynamic K-factor: higher impact for more significant results
|
||||
const kFactor = K_BASE * playerCount;
|
||||
const eloChange = kFactor * (actualScore - expectedScore);
|
||||
const newElo = Math.max(100, Math.round(player.elo + eloChange));
|
||||
// Dynamic K-factor: higher impact for more significant results
|
||||
const kFactor = K_BASE * playerCount;
|
||||
const eloChange = kFactor * (actualScore - expectedScore);
|
||||
const newElo = Math.max(100, Math.round(player.elo + eloChange));
|
||||
|
||||
if (!isNaN(newElo)) {
|
||||
console.log(`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`);
|
||||
updateElo.run({ id: player.id, elo: newElo });
|
||||
if (!isNaN(newElo)) {
|
||||
console.log(
|
||||
`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`,
|
||||
);
|
||||
updateElo.run({ id: player.id, elo: newElo });
|
||||
|
||||
insertGame.run({
|
||||
id: `${player.id}-poker-${Date.now()}`,
|
||||
p1: player.id,
|
||||
p2: null, // No single opponent
|
||||
p1_score: actualScore,
|
||||
p2_score: null,
|
||||
p1_elo: player.elo,
|
||||
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
|
||||
p1_new_elo: newElo,
|
||||
p2_new_elo: null,
|
||||
type: 'POKER_ROUND',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.error(`Error calculating new Elo for ${player.globalName}.`);
|
||||
}
|
||||
});
|
||||
insertGame.run({
|
||||
id: `${player.id}-poker-${Date.now()}`,
|
||||
p1: player.id,
|
||||
p2: null, // No single opponent
|
||||
p1_score: actualScore,
|
||||
p2_score: null,
|
||||
p1_elo: player.elo,
|
||||
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
|
||||
p1_new_elo: newElo,
|
||||
p2_new_elo: null,
|
||||
type: "POKER_ROUND",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
console.error(`Error calculating new Elo for ${player.globalName}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import {
|
||||
getUser,
|
||||
updateUserCoins,
|
||||
insertLog,
|
||||
getAllSkins,
|
||||
insertSOTD,
|
||||
clearSOTDStats,
|
||||
getAllSOTDStats, deleteSOTD, insertGame,
|
||||
} from '../database/index.js';
|
||||
import { messagesTimestamps, activeSlowmodes, skins } from './state.js';
|
||||
import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js';
|
||||
getUser,
|
||||
updateUserCoins,
|
||||
insertLog,
|
||||
getAllSkins,
|
||||
insertSOTD,
|
||||
clearSOTDStats,
|
||||
getAllSOTDStats,
|
||||
deleteSOTD,
|
||||
insertGame,
|
||||
} from "../database/index.js";
|
||||
import { messagesTimestamps, activeSlowmodes, skins } from "./state.js";
|
||||
import { deal, createSeededRNG, seededShuffle, createDeck } from "./solitaire.js";
|
||||
|
||||
/**
|
||||
* Handles awarding points (coins) to users for their message activity.
|
||||
@@ -17,53 +19,53 @@ import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js
|
||||
* @returns {boolean} True if points were awarded, false otherwise.
|
||||
*/
|
||||
export async function channelPointsHandler(message) {
|
||||
const author = message.author;
|
||||
const authorDB = getUser.get(author.id);
|
||||
const author = message.author;
|
||||
const authorDB = getUser.get(author.id);
|
||||
|
||||
if (!authorDB) {
|
||||
// User not in our database, do nothing.
|
||||
return false;
|
||||
}
|
||||
if (!authorDB) {
|
||||
// User not in our database, do nothing.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore short messages or commands that might be spammed
|
||||
if (message.content.length < 3 || message.content.startsWith('?')) {
|
||||
return false;
|
||||
}
|
||||
// Ignore short messages or commands that might be spammed
|
||||
if (message.content.length < 3 || message.content.startsWith("?")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const userTimestamps = messagesTimestamps.get(author.id) || [];
|
||||
const now = Date.now();
|
||||
const userTimestamps = messagesTimestamps.get(author.id) || [];
|
||||
|
||||
// Filter out timestamps older than 15 minutes (900,000 ms)
|
||||
const recentTimestamps = userTimestamps.filter(ts => now - ts < 900000);
|
||||
// Filter out timestamps older than 15 minutes (900,000 ms)
|
||||
const recentTimestamps = userTimestamps.filter((ts) => now - ts < 900000);
|
||||
|
||||
// If the user has already sent 10 messages in the last 15 mins, do nothing
|
||||
if (recentTimestamps.length >= 10) {
|
||||
return false;
|
||||
}
|
||||
// If the user has already sent 10 messages in the last 15 mins, do nothing
|
||||
if (recentTimestamps.length >= 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the new message timestamp
|
||||
recentTimestamps.push(now);
|
||||
messagesTimestamps.set(author.id, recentTimestamps);
|
||||
// Add the new message timestamp
|
||||
recentTimestamps.push(now);
|
||||
messagesTimestamps.set(author.id, recentTimestamps);
|
||||
|
||||
// Award 50 coins for the 10th message, 10 for others
|
||||
const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10;
|
||||
const newCoinTotal = authorDB.coins + coinsToAdd;
|
||||
// Award 50 coins for the 10th message, 10 for others
|
||||
const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10;
|
||||
const newCoinTotal = authorDB.coins + coinsToAdd;
|
||||
|
||||
updateUserCoins.run({
|
||||
id: author.id,
|
||||
coins: newCoinTotal,
|
||||
});
|
||||
updateUserCoins.run({
|
||||
id: author.id,
|
||||
coins: newCoinTotal,
|
||||
});
|
||||
|
||||
insertLog.run({
|
||||
id: `${author.id}-${now}`,
|
||||
user_id: author.id,
|
||||
action: 'AUTO_COINS',
|
||||
target_user_id: null,
|
||||
coins_amount: coinsToAdd,
|
||||
user_new_amount: newCoinTotal,
|
||||
});
|
||||
insertLog.run({
|
||||
id: `${author.id}-${now}`,
|
||||
user_id: author.id,
|
||||
action: "AUTO_COINS",
|
||||
target_user_id: null,
|
||||
coins_amount: coinsToAdd,
|
||||
user_new_amount: newCoinTotal,
|
||||
});
|
||||
|
||||
return true; // Indicate that points were awarded
|
||||
return true; // Indicate that points were awarded
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,37 +74,37 @@ export async function channelPointsHandler(message) {
|
||||
* @returns {object} An object indicating if a message was deleted or a slowmode expired.
|
||||
*/
|
||||
export async function slowmodesHandler(message) {
|
||||
const author = message.author;
|
||||
const authorSlowmode = activeSlowmodes[author.id];
|
||||
const author = message.author;
|
||||
const authorSlowmode = activeSlowmodes[author.id];
|
||||
|
||||
if (!authorSlowmode) {
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
if (!authorSlowmode) {
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const now = Date.now();
|
||||
|
||||
// Check if the slowmode duration has passed
|
||||
if (now > authorSlowmode.endAt) {
|
||||
console.log(`Slowmode for ${author.username} has expired.`);
|
||||
delete activeSlowmodes[author.id];
|
||||
return { deleted: false, expired: true };
|
||||
}
|
||||
// Check if the slowmode duration has passed
|
||||
if (now > authorSlowmode.endAt) {
|
||||
console.log(`Slowmode for ${author.username} has expired.`);
|
||||
delete activeSlowmodes[author.id];
|
||||
return { deleted: false, expired: true };
|
||||
}
|
||||
|
||||
// Check if the user is messaging too quickly (less than 1 minute between messages)
|
||||
if (authorSlowmode.lastMessage && (now - authorSlowmode.lastMessage < 60 * 1000)) {
|
||||
try {
|
||||
await message.delete();
|
||||
console.log(`Deleted a message from slowmoded user: ${author.username}`);
|
||||
return { deleted: true, expired: false };
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete slowmode message:`, err);
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
} else {
|
||||
// Update the last message timestamp for the user
|
||||
authorSlowmode.lastMessage = now;
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
// Check if the user is messaging too quickly (less than 1 minute between messages)
|
||||
if (authorSlowmode.lastMessage && now - authorSlowmode.lastMessage < 60 * 1000) {
|
||||
try {
|
||||
await message.delete();
|
||||
console.log(`Deleted a message from slowmoded user: ${author.username}`);
|
||||
return { deleted: true, expired: false };
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete slowmode message:`, err);
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
} else {
|
||||
// Update the last message timestamp for the user
|
||||
authorSlowmode.lastMessage = now;
|
||||
return { deleted: false, expired: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,27 +113,27 @@ export async function slowmodesHandler(message) {
|
||||
* @returns {string} The calculated random price as a string.
|
||||
*/
|
||||
export function randomSkinPrice() {
|
||||
const dbSkins = getAllSkins.all();
|
||||
if (dbSkins.length === 0) return '0.00';
|
||||
const dbSkins = getAllSkins.all();
|
||||
if (dbSkins.length === 0) return "0.00";
|
||||
|
||||
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
|
||||
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
|
||||
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
|
||||
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
|
||||
|
||||
if (!randomSkinData) return '0.00';
|
||||
if (!randomSkinData) return "0.00";
|
||||
|
||||
// Generate random level and chroma
|
||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||
let randomChroma = 1;
|
||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||
}
|
||||
// Generate random level and chroma
|
||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||
let randomChroma = 1;
|
||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||
}
|
||||
|
||||
// Calculate price based on these random values
|
||||
let result = parseFloat(randomDbSkin.basePrice);
|
||||
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
|
||||
result *= (1 + (randomChroma / 4));
|
||||
// Calculate price based on these random values
|
||||
let result = parseFloat(randomDbSkin.basePrice);
|
||||
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
|
||||
result *= 1 + randomChroma / 4;
|
||||
|
||||
return result.toFixed(0);
|
||||
return result.toFixed(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,68 +141,70 @@ export function randomSkinPrice() {
|
||||
* This function clears previous stats, awards the winner, and generates a new daily seed.
|
||||
*/
|
||||
export function initTodaysSOTD() {
|
||||
console.log('Initializing new Solitaire of the Day...');
|
||||
console.log("Initializing new Solitaire of the Day...");
|
||||
|
||||
// 1. Award previous day's winner
|
||||
const rankings = getAllSOTDStats.all();
|
||||
if (rankings.length > 0) {
|
||||
const winnerId = rankings[0].user_id;
|
||||
const winnerUser = getUser.get(winnerId);
|
||||
// 1. Award previous day's winner
|
||||
const rankings = getAllSOTDStats.all();
|
||||
if (rankings.length > 0) {
|
||||
const winnerId = rankings[0].user_id;
|
||||
const winnerUser = getUser.get(winnerId);
|
||||
|
||||
if (winnerUser) {
|
||||
const reward = 1000;
|
||||
const newCoinTotal = winnerUser.coins + reward;
|
||||
updateUserCoins.run({ id: winnerId, coins: newCoinTotal });
|
||||
insertLog.run({
|
||||
id: `${winnerId}-sotd-win-${Date.now()}`,
|
||||
target_user_id: null,
|
||||
user_id: winnerId,
|
||||
action: 'SOTD_FIRST_PLACE',
|
||||
coins_amount: reward,
|
||||
user_new_amount: newCoinTotal,
|
||||
});
|
||||
console.log(`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`);
|
||||
insertGame.run({
|
||||
id: `${winnerId}-${Date.now()}`,
|
||||
p1: winnerId,
|
||||
p2: null,
|
||||
p1_score: rankings[0].score,
|
||||
p2_score: null,
|
||||
p1_elo: winnerUser.elo,
|
||||
p2_elo: null,
|
||||
p1_new_elo: winnerUser.elo,
|
||||
p2_new_elo: null,
|
||||
type: 'SOTD',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (winnerUser) {
|
||||
const reward = 1000;
|
||||
const newCoinTotal = winnerUser.coins + reward;
|
||||
updateUserCoins.run({ id: winnerId, coins: newCoinTotal });
|
||||
insertLog.run({
|
||||
id: `${winnerId}-sotd-win-${Date.now()}`,
|
||||
target_user_id: null,
|
||||
user_id: winnerId,
|
||||
action: "SOTD_FIRST_PLACE",
|
||||
coins_amount: reward,
|
||||
user_new_amount: newCoinTotal,
|
||||
});
|
||||
console.log(
|
||||
`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`,
|
||||
);
|
||||
insertGame.run({
|
||||
id: `${winnerId}-${Date.now()}`,
|
||||
p1: winnerId,
|
||||
p2: null,
|
||||
p1_score: rankings[0].score,
|
||||
p2_score: null,
|
||||
p1_elo: winnerUser.elo,
|
||||
p2_elo: null,
|
||||
p1_new_elo: winnerUser.elo,
|
||||
p2_new_elo: null,
|
||||
type: "SOTD",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Generate a new seeded deck for today
|
||||
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
let numericSeed = 0;
|
||||
for (let i = 0; i < newRandomSeed.length; i++) {
|
||||
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF;
|
||||
}
|
||||
// 2. Generate a new seeded deck for today
|
||||
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
let numericSeed = 0;
|
||||
for (let i = 0; i < newRandomSeed.length; i++) {
|
||||
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xffffffff;
|
||||
}
|
||||
|
||||
const rng = createSeededRNG(numericSeed);
|
||||
const deck = createDeck();
|
||||
const shuffledDeck = seededShuffle(deck, rng);
|
||||
const todaysSOTD = deal(shuffledDeck);
|
||||
const rng = createSeededRNG(numericSeed);
|
||||
const deck = createDeck();
|
||||
const shuffledDeck = seededShuffle(deck, rng);
|
||||
const todaysSOTD = deal(shuffledDeck);
|
||||
|
||||
// 3. Clear old stats and save the new game state to the database
|
||||
try {
|
||||
clearSOTDStats.run();
|
||||
deleteSOTD.run();
|
||||
insertSOTD.run({
|
||||
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
|
||||
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
|
||||
stockPile: JSON.stringify(todaysSOTD.stockPile),
|
||||
wastePile: JSON.stringify(todaysSOTD.wastePile),
|
||||
seed: newRandomSeed,
|
||||
});
|
||||
console.log("Today's SOTD is ready with a new seed.");
|
||||
} catch(e) {
|
||||
console.error("Error saving new SOTD to database:", e);
|
||||
}
|
||||
// 3. Clear old stats and save the new game state to the database
|
||||
try {
|
||||
clearSOTDStats.run();
|
||||
deleteSOTD.run();
|
||||
insertSOTD.run({
|
||||
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
|
||||
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
|
||||
stockPile: JSON.stringify(todaysSOTD.stockPile),
|
||||
wastePile: JSON.stringify(todaysSOTD.wastePile),
|
||||
seed: newRandomSeed,
|
||||
});
|
||||
console.log("Today's SOTD is ready with a new seed.");
|
||||
} catch (e) {
|
||||
console.error("Error saving new SOTD to database:", e);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,60 @@
|
||||
import pkg from 'pokersolver';
|
||||
import pkg from "pokersolver";
|
||||
const { Hand } = pkg;
|
||||
|
||||
// An array of all 52 standard playing cards.
|
||||
export const initialCards = [
|
||||
'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd',
|
||||
'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks',
|
||||
'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc',
|
||||
'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh',
|
||||
"Ad",
|
||||
"2d",
|
||||
"3d",
|
||||
"4d",
|
||||
"5d",
|
||||
"6d",
|
||||
"7d",
|
||||
"8d",
|
||||
"9d",
|
||||
"Td",
|
||||
"Jd",
|
||||
"Qd",
|
||||
"Kd",
|
||||
"As",
|
||||
"2s",
|
||||
"3s",
|
||||
"4s",
|
||||
"5s",
|
||||
"6s",
|
||||
"7s",
|
||||
"8s",
|
||||
"9s",
|
||||
"Ts",
|
||||
"Js",
|
||||
"Qs",
|
||||
"Ks",
|
||||
"Ac",
|
||||
"2c",
|
||||
"3c",
|
||||
"4c",
|
||||
"5c",
|
||||
"6c",
|
||||
"7c",
|
||||
"8c",
|
||||
"9c",
|
||||
"Tc",
|
||||
"Jc",
|
||||
"Qc",
|
||||
"Kc",
|
||||
"Ah",
|
||||
"2h",
|
||||
"3h",
|
||||
"4h",
|
||||
"5h",
|
||||
"6h",
|
||||
"7h",
|
||||
"8h",
|
||||
"9h",
|
||||
"Th",
|
||||
"Jh",
|
||||
"Qh",
|
||||
"Kh",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -14,8 +62,8 @@ export const initialCards = [
|
||||
* @returns {Array<string>} A new array containing all 52 cards in a random order.
|
||||
*/
|
||||
export function initialShuffledCards() {
|
||||
// Create a copy and sort it randomly
|
||||
return [...initialCards].sort(() => 0.5 - Math.random());
|
||||
// Create a copy and sort it randomly
|
||||
return [...initialCards].sort(() => 0.5 - Math.random());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,19 +73,19 @@ export function initialShuffledCards() {
|
||||
* @returns {string|null} The ID of the next player, or null if none is found.
|
||||
*/
|
||||
export function getFirstActivePlayerAfterDealer(room) {
|
||||
const players = Object.values(room.players);
|
||||
const dealerPosition = players.findIndex((p) => p.id === room.dealer);
|
||||
const players = Object.values(room.players);
|
||||
const dealerPosition = players.findIndex((p) => p.id === room.dealer);
|
||||
|
||||
// Loop through players starting from the one after the dealer
|
||||
for (let i = 1; i <= players.length; i++) {
|
||||
const nextPos = (dealerPosition + i) % players.length;
|
||||
const nextPlayer = players[nextPos];
|
||||
// Player must not be folded or all-in to be able to act
|
||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||
return nextPlayer.id;
|
||||
}
|
||||
}
|
||||
return null; // Should not happen in a normal game
|
||||
// Loop through players starting from the one after the dealer
|
||||
for (let i = 1; i <= players.length; i++) {
|
||||
const nextPos = (dealerPosition + i) % players.length;
|
||||
const nextPlayer = players[nextPos];
|
||||
// Player must not be folded or all-in to be able to act
|
||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||
return nextPlayer.id;
|
||||
}
|
||||
}
|
||||
return null; // Should not happen in a normal game
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,18 +94,18 @@ export function getFirstActivePlayerAfterDealer(room) {
|
||||
* @returns {string|null} The ID of the next player, or null if none is found.
|
||||
*/
|
||||
export function getNextActivePlayer(room) {
|
||||
const players = Object.values(room.players);
|
||||
const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player);
|
||||
const players = Object.values(room.players);
|
||||
const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player);
|
||||
|
||||
// Loop through players starting from the one after the current player
|
||||
for (let i = 1; i <= players.length; i++) {
|
||||
const nextPos = (currentPlayerPosition + i) % players.length;
|
||||
const nextPlayer = players[nextPos];
|
||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||
return nextPlayer.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// Loop through players starting from the one after the current player
|
||||
for (let i = 1; i <= players.length; i++) {
|
||||
const nextPos = (currentPlayerPosition + i) % players.length;
|
||||
const nextPlayer = players[nextPos];
|
||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||
return nextPlayer.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,40 +114,54 @@ export function getNextActivePlayer(room) {
|
||||
* @returns {object} An object with `endRound`, `winner`, and `nextPhase` properties.
|
||||
*/
|
||||
export function checkEndOfBettingRound(room) {
|
||||
const activePlayers = Object.values(room.players).filter((p) => !p.folded);
|
||||
const activePlayers = Object.values(room.players).filter((p) => !p.folded);
|
||||
|
||||
// --- Scenario 1: Only one player left (everyone else folded) ---
|
||||
if (activePlayers.length === 1) {
|
||||
return { endRound: true, winner: activePlayers[0].id, nextPhase: 'showdown' };
|
||||
}
|
||||
// --- Scenario 1: Only one player left (everyone else folded) ---
|
||||
if (activePlayers.length === 1) {
|
||||
return {
|
||||
endRound: true,
|
||||
winner: activePlayers[0].id,
|
||||
nextPhase: "showdown",
|
||||
};
|
||||
}
|
||||
|
||||
// --- Scenario 2: All remaining players are all-in ---
|
||||
// The hand goes immediately to a "progressive showdown".
|
||||
const allInPlayers = activePlayers.filter(p => p.allin);
|
||||
if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) {
|
||||
return { endRound: true, winner: null, nextPhase: 'progressive-showdown' };
|
||||
}
|
||||
// --- Scenario 2: All remaining players are all-in ---
|
||||
// The hand goes immediately to a "progressive showdown".
|
||||
const allInPlayers = activePlayers.filter((p) => p.allin);
|
||||
if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) {
|
||||
return { endRound: true, winner: null, nextPhase: "progressive-showdown" };
|
||||
}
|
||||
|
||||
// --- Scenario 3: All active players have acted and bets are equal ---
|
||||
const allBetsMatched = activePlayers.every(p =>
|
||||
p.allin || // Player is all-in
|
||||
(p.bet === room.highest_bet && p.last_played_turn === room.current_turn) // Or their bet matches the highest and they've acted this turn
|
||||
);
|
||||
// --- Scenario 3: All active players have acted and bets are equal ---
|
||||
const allBetsMatched = activePlayers.every(
|
||||
(p) =>
|
||||
p.allin || // Player is all-in
|
||||
(p.bet === room.highest_bet && p.last_played_turn === room.current_turn), // Or their bet matches the highest and they've acted this turn
|
||||
);
|
||||
|
||||
if (allBetsMatched) {
|
||||
let nextPhase;
|
||||
switch (room.current_turn) {
|
||||
case 0: nextPhase = 'flop'; break;
|
||||
case 1: nextPhase = 'turn'; break;
|
||||
case 2: nextPhase = 'river'; break;
|
||||
case 3: nextPhase = 'showdown'; break;
|
||||
default: nextPhase = null; // Should not happen
|
||||
}
|
||||
return { endRound: true, winner: null, nextPhase: nextPhase };
|
||||
}
|
||||
if (allBetsMatched) {
|
||||
let nextPhase;
|
||||
switch (room.current_turn) {
|
||||
case 0:
|
||||
nextPhase = "flop";
|
||||
break;
|
||||
case 1:
|
||||
nextPhase = "turn";
|
||||
break;
|
||||
case 2:
|
||||
nextPhase = "river";
|
||||
break;
|
||||
case 3:
|
||||
nextPhase = "showdown";
|
||||
break;
|
||||
default:
|
||||
nextPhase = null; // Should not happen
|
||||
}
|
||||
return { endRound: true, winner: null, nextPhase: nextPhase };
|
||||
}
|
||||
|
||||
// --- Default: The round continues ---
|
||||
return { endRound: false, winner: null, nextPhase: null };
|
||||
// --- Default: The round continues ---
|
||||
return { endRound: false, winner: null, nextPhase: null };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,32 +170,35 @@ export function checkEndOfBettingRound(room) {
|
||||
* @returns {Array<string>} An array of winner IDs. Can contain multiple IDs in case of a split pot.
|
||||
*/
|
||||
export function checkRoomWinners(room) {
|
||||
const communityCards = room.tapis;
|
||||
const activePlayers = Object.values(room.players).filter(p => !p.folded);
|
||||
const communityCards = room.tapis;
|
||||
const activePlayers = Object.values(room.players).filter((p) => !p.folded);
|
||||
|
||||
// Solve each player's hand to find the best possible 5-card combination
|
||||
const playerSolutions = activePlayers.map(player => ({
|
||||
id: player.id,
|
||||
solution: Hand.solve([...communityCards, ...player.hand]),
|
||||
}));
|
||||
// Solve each player's hand to find the best possible 5-card combination
|
||||
const playerSolutions = activePlayers.map((player) => ({
|
||||
id: player.id,
|
||||
solution: Hand.solve([...communityCards, ...player.hand]),
|
||||
}));
|
||||
|
||||
if (playerSolutions.length === 0) return [];
|
||||
if (playerSolutions.length === 0) return [];
|
||||
|
||||
// Use pokersolver's `Hand.winners()` to find the best hand(s)
|
||||
const winningSolutions = Hand.winners(playerSolutions.map(ps => ps.solution));
|
||||
// Use pokersolver's `Hand.winners()` to find the best hand(s)
|
||||
const winningSolutions = Hand.winners(playerSolutions.map((ps) => ps.solution));
|
||||
|
||||
// Find the player IDs that correspond to the winning hand solutions
|
||||
const winnerIds = [];
|
||||
for (const winningHand of winningSolutions) {
|
||||
for (const playerSol of playerSolutions) {
|
||||
// Compare description and card pool to uniquely identify the hand
|
||||
if (playerSol.solution.descr === winningHand.descr && playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()) {
|
||||
if (!winnerIds.includes(playerSol.id)) {
|
||||
winnerIds.push(playerSol.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Find the player IDs that correspond to the winning hand solutions
|
||||
const winnerIds = [];
|
||||
for (const winningHand of winningSolutions) {
|
||||
for (const playerSol of playerSolutions) {
|
||||
// Compare description and card pool to uniquely identify the hand
|
||||
if (
|
||||
playerSol.solution.descr === winningHand.descr &&
|
||||
playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()
|
||||
) {
|
||||
if (!winnerIds.includes(playerSol.id)) {
|
||||
winnerIds.push(playerSol.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return winnerIds;
|
||||
return winnerIds;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// --- Constants for Deck Creation ---
|
||||
import {sleep} from "openai/core";
|
||||
import {emitSolitaireUpdate, emitUpdate} from "../server/socket.js";
|
||||
import { sleep } from "openai/core";
|
||||
import { emitSolitaireUpdate, emitUpdate } from "../server/socket.js";
|
||||
|
||||
const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs
|
||||
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
|
||||
const SUITS = ["h", "d", "s", "c"]; // Hearts, Diamonds, Spades, Clubs
|
||||
const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
|
||||
|
||||
// --- Helper Functions for Card Logic ---
|
||||
|
||||
@@ -13,12 +13,12 @@ const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
|
||||
* @returns {number} The numeric value (Ace=1, King=13).
|
||||
*/
|
||||
function getRankValue(rank) {
|
||||
if (rank === 'A') return 1;
|
||||
if (rank === 'T') return 10;
|
||||
if (rank === 'J') return 11;
|
||||
if (rank === 'Q') return 12;
|
||||
if (rank === 'K') return 13;
|
||||
return parseInt(rank, 10);
|
||||
if (rank === "A") return 1;
|
||||
if (rank === "T") return 10;
|
||||
if (rank === "J") return 11;
|
||||
if (rank === "Q") return 12;
|
||||
if (rank === "K") return 13;
|
||||
return parseInt(rank, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,10 +27,9 @@ function getRankValue(rank) {
|
||||
* @returns {string} 'red' or 'black'.
|
||||
*/
|
||||
function getCardColor(suit) {
|
||||
return (suit === 'h' || suit === 'd') ? 'red' : 'black';
|
||||
return suit === "h" || suit === "d" ? "red" : "black";
|
||||
}
|
||||
|
||||
|
||||
// --- Core Game Logic Functions ---
|
||||
|
||||
/**
|
||||
@@ -38,13 +37,13 @@ function getCardColor(suit) {
|
||||
* @returns {Array<Object>} The unshuffled deck of cards.
|
||||
*/
|
||||
export function createDeck() {
|
||||
const deck = [];
|
||||
for (const suit of SUITS) {
|
||||
for (const rank of RANKS) {
|
||||
deck.push({ suit, rank, faceUp: false });
|
||||
}
|
||||
}
|
||||
return deck;
|
||||
const deck = [];
|
||||
for (const suit of SUITS) {
|
||||
for (const rank of RANKS) {
|
||||
deck.push({ suit, rank, faceUp: false });
|
||||
}
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,16 +52,16 @@ export function createDeck() {
|
||||
* @returns {Array} The shuffled array (mutated in place).
|
||||
*/
|
||||
export function shuffle(array) {
|
||||
let currentIndex = array.length;
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element.
|
||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
return array;
|
||||
let currentIndex = array.length;
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element.
|
||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,12 +70,12 @@ export function shuffle(array) {
|
||||
* @returns {function} A function that returns a pseudorandom number between 0 and 1.
|
||||
*/
|
||||
export function createSeededRNG(seed) {
|
||||
return function() {
|
||||
let t = seed += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ t >>> 15, t | 1);
|
||||
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||||
};
|
||||
return function () {
|
||||
let t = (seed += 0x6d2b79f5);
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,16 +85,16 @@ export function createSeededRNG(seed) {
|
||||
* @returns {Array} The shuffled array (mutated in place).
|
||||
*/
|
||||
export function seededShuffle(array, rng) {
|
||||
let currentIndex = array.length;
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element using the seeded RNG.
|
||||
const randomIndex = Math.floor(rng() * currentIndex);
|
||||
currentIndex--;
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
return array;
|
||||
let currentIndex = array.length;
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element using the seeded RNG.
|
||||
const randomIndex = Math.floor(rng() * currentIndex);
|
||||
currentIndex--;
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,31 +103,31 @@ export function seededShuffle(array, rng) {
|
||||
* @returns {Object} The initial gameState object for Klondike Solitaire.
|
||||
*/
|
||||
export function deal(deck) {
|
||||
const gameState = {
|
||||
tableauPiles: [[], [], [], [], [], [], []],
|
||||
foundationPiles: [[], [], [], []],
|
||||
stockPile: [],
|
||||
wastePile: [],
|
||||
};
|
||||
const gameState = {
|
||||
tableauPiles: [[], [], [], [], [], [], []],
|
||||
foundationPiles: [[], [], [], []],
|
||||
stockPile: [],
|
||||
wastePile: [],
|
||||
};
|
||||
|
||||
// Deal cards to the 7 tableau piles
|
||||
for (let i = 0; i < 7; i++) {
|
||||
for (let j = i; j < 7; j++) {
|
||||
gameState.tableauPiles[j].push(deck.shift());
|
||||
}
|
||||
}
|
||||
// Deal cards to the 7 tableau piles
|
||||
for (let i = 0; i < 7; i++) {
|
||||
for (let j = i; j < 7; j++) {
|
||||
gameState.tableauPiles[j].push(deck.shift());
|
||||
}
|
||||
}
|
||||
|
||||
// Flip the top card of each tableau pile
|
||||
gameState.tableauPiles.forEach(pile => {
|
||||
if (pile.length > 0) {
|
||||
pile[pile.length - 1].faceUp = true;
|
||||
}
|
||||
});
|
||||
// Flip the top card of each tableau pile
|
||||
gameState.tableauPiles.forEach((pile) => {
|
||||
if (pile.length > 0) {
|
||||
pile[pile.length - 1].faceUp = true;
|
||||
}
|
||||
});
|
||||
|
||||
// The rest of the deck becomes the stock
|
||||
gameState.stockPile = deck;
|
||||
// The rest of the deck becomes the stock
|
||||
gameState.stockPile = deck;
|
||||
|
||||
return gameState;
|
||||
return gameState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,59 +137,59 @@ export function deal(deck) {
|
||||
* @returns {boolean} True if the move is valid, false otherwise.
|
||||
*/
|
||||
export function isValidMove(gameState, moveData) {
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
|
||||
// --- Get Source Pile and Card ---
|
||||
let sourcePile;
|
||||
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
|
||||
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
else return false; // Invalid source type
|
||||
// --- Get Source Pile and Card ---
|
||||
let sourcePile;
|
||||
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
|
||||
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
else return false; // Invalid source type
|
||||
|
||||
const sourceCard = sourcePile?.[sourceCardIndex];
|
||||
if (!sourceCard || !sourceCard.faceUp) {
|
||||
return false; // Cannot move a card that doesn't exist or is face-down
|
||||
}
|
||||
const sourceCard = sourcePile?.[sourceCardIndex];
|
||||
if (!sourceCard || !sourceCard.faceUp) {
|
||||
return false; // Cannot move a card that doesn't exist or is face-down
|
||||
}
|
||||
|
||||
// --- Validate Move TO a Tableau Pile ---
|
||||
if (destPileType === 'tableauPiles') {
|
||||
const destinationPile = gameState.tableauPiles[destPileIndex];
|
||||
const topCard = destinationPile[destinationPile.length - 1];
|
||||
// --- Validate Move TO a Tableau Pile ---
|
||||
if (destPileType === "tableauPiles") {
|
||||
const destinationPile = gameState.tableauPiles[destPileIndex];
|
||||
const topCard = destinationPile[destinationPile.length - 1];
|
||||
|
||||
if (!topCard) {
|
||||
// If the destination tableau is empty, only a King can be moved there.
|
||||
return sourceCard.rank === 'K';
|
||||
}
|
||||
if (!topCard) {
|
||||
// If the destination tableau is empty, only a King can be moved there.
|
||||
return sourceCard.rank === "K";
|
||||
}
|
||||
|
||||
// Card must be opposite color and one rank lower than the destination top card.
|
||||
const sourceColor = getCardColor(sourceCard.suit);
|
||||
const destColor = getCardColor(topCard.suit);
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
return sourceColor !== destColor && destValue - sourceValue === 1;
|
||||
}
|
||||
// Card must be opposite color and one rank lower than the destination top card.
|
||||
const sourceColor = getCardColor(sourceCard.suit);
|
||||
const destColor = getCardColor(topCard.suit);
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
return sourceColor !== destColor && destValue - sourceValue === 1;
|
||||
}
|
||||
|
||||
// --- Validate Move TO a Foundation Pile ---
|
||||
if (destPileType === 'foundationPiles') {
|
||||
// You can only move one card at a time to a foundation pile.
|
||||
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
|
||||
if (stackBeingMoved.length > 1) return false;
|
||||
// --- Validate Move TO a Foundation Pile ---
|
||||
if (destPileType === "foundationPiles") {
|
||||
// You can only move one card at a time to a foundation pile.
|
||||
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
|
||||
if (stackBeingMoved.length > 1) return false;
|
||||
|
||||
const destinationPile = gameState.foundationPiles[destPileIndex];
|
||||
const topCard = destinationPile[destinationPile.length - 1];
|
||||
const destinationPile = gameState.foundationPiles[destPileIndex];
|
||||
const topCard = destinationPile[destinationPile.length - 1];
|
||||
|
||||
if (!topCard) {
|
||||
// If the foundation is empty, only an Ace of any suit can be moved there.
|
||||
return sourceCard.rank === 'A';
|
||||
}
|
||||
if (!topCard) {
|
||||
// If the foundation is empty, only an Ace of any suit can be moved there.
|
||||
return sourceCard.rank === "A";
|
||||
}
|
||||
|
||||
// Card must be the same suit and one rank higher.
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
return sourceCard.suit === topCard.suit && sourceValue - destValue === 1;
|
||||
}
|
||||
// Card must be the same suit and one rank higher.
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
return sourceCard.suit === topCard.suit && sourceValue - destValue === 1;
|
||||
}
|
||||
|
||||
return false; // Invalid destination type
|
||||
return false; // Invalid destination type
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,41 +198,41 @@ export function isValidMove(gameState, moveData) {
|
||||
* @param {Object} moveData - The details of the move.
|
||||
*/
|
||||
export function moveCard(gameState, moveData) {
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
|
||||
let sourcePile;
|
||||
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
|
||||
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
let sourcePile;
|
||||
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
|
||||
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
|
||||
let destPile;
|
||||
if (destPileType === 'tableauPiles') destPile = gameState.tableauPiles[destPileIndex];
|
||||
else if (destPileType === 'foundationPiles') destPile = gameState.foundationPiles[destPileIndex];
|
||||
let destPile;
|
||||
if (destPileType === "tableauPiles") destPile = gameState.tableauPiles[destPileIndex];
|
||||
else if (destPileType === "foundationPiles") destPile = gameState.foundationPiles[destPileIndex];
|
||||
|
||||
// Cut the entire stack of cards to be moved from the source pile.
|
||||
const cardsToMove = sourcePile.splice(sourceCardIndex);
|
||||
// Add the stack to the destination pile.
|
||||
destPile.push(...cardsToMove);
|
||||
// Cut the entire stack of cards to be moved from the source pile.
|
||||
const cardsToMove = sourcePile.splice(sourceCardIndex);
|
||||
// Add the stack to the destination pile.
|
||||
destPile.push(...cardsToMove);
|
||||
|
||||
const histMove = {
|
||||
move: 'move',
|
||||
sourcePileType: sourcePileType,
|
||||
sourcePileIndex: sourcePileIndex,
|
||||
sourceCardIndex: sourceCardIndex,
|
||||
destPileType: destPileType,
|
||||
destPileIndex: destPileIndex,
|
||||
cardsMoved: cardsToMove,
|
||||
cardWasFlipped: false,
|
||||
points: destPileType === 'foundationPiles' ? 11 : 1 // Points for moving to foundation
|
||||
}
|
||||
const histMove = {
|
||||
move: "move",
|
||||
sourcePileType: sourcePileType,
|
||||
sourcePileIndex: sourcePileIndex,
|
||||
sourceCardIndex: sourceCardIndex,
|
||||
destPileType: destPileType,
|
||||
destPileIndex: destPileIndex,
|
||||
cardsMoved: cardsToMove,
|
||||
cardWasFlipped: false,
|
||||
points: destPileType === "foundationPiles" ? 11 : 1, // Points for moving to foundation
|
||||
};
|
||||
|
||||
// If the source was a tableau pile and there are cards left, flip the new top card.
|
||||
if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) {
|
||||
sourcePile[sourcePile.length - 1].faceUp = true;
|
||||
histMove.cardWasFlipped = true;
|
||||
}
|
||||
// If the source was a tableau pile and there are cards left, flip the new top card.
|
||||
if (sourcePileType === "tableauPiles" && sourcePile.length > 0) {
|
||||
sourcePile[sourcePile.length - 1].faceUp = true;
|
||||
histMove.cardWasFlipped = true;
|
||||
}
|
||||
|
||||
gameState.hist.push(histMove)
|
||||
gameState.hist.push(histMove);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,52 +240,51 @@ export function moveCard(gameState, moveData) {
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
*/
|
||||
export function drawCard(gameState) {
|
||||
if (gameState.stockPile.length > 0) {
|
||||
const card = gameState.stockPile.pop();
|
||||
card.faceUp = true;
|
||||
gameState.wastePile.push(card);
|
||||
gameState.hist.push({
|
||||
move: 'draw',
|
||||
card: card
|
||||
})
|
||||
} else if (gameState.wastePile.length > 0) {
|
||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||
gameState.stockPile = gameState.wastePile.reverse();
|
||||
gameState.stockPile.forEach(card => (card.faceUp = false));
|
||||
gameState.wastePile = [];
|
||||
gameState.hist.push({
|
||||
move: 'draw-reset',
|
||||
})
|
||||
}
|
||||
if (gameState.stockPile.length > 0) {
|
||||
const card = gameState.stockPile.pop();
|
||||
card.faceUp = true;
|
||||
gameState.wastePile.push(card);
|
||||
gameState.hist.push({
|
||||
move: "draw",
|
||||
card: card,
|
||||
});
|
||||
} else if (gameState.wastePile.length > 0) {
|
||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||
gameState.stockPile = gameState.wastePile.reverse();
|
||||
gameState.stockPile.forEach((card) => (card.faceUp = false));
|
||||
gameState.wastePile = [];
|
||||
gameState.hist.push({
|
||||
move: "draw-reset",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function draw3Cards(gameState) {
|
||||
if (gameState.stockPile.length > 0) {
|
||||
let cards = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (gameState.stockPile.length > 0) {
|
||||
const card = gameState.stockPile.pop();
|
||||
card.faceUp = true;
|
||||
gameState.wastePile.push(card);
|
||||
cards.push(card);
|
||||
} else {
|
||||
break; // Stop if stock runs out
|
||||
}
|
||||
}
|
||||
gameState.hist.push({
|
||||
move: 'draw-3',
|
||||
cards: cards,
|
||||
})
|
||||
} else if (gameState.wastePile.length > 0) {
|
||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||
gameState.stockPile = gameState.wastePile.reverse();
|
||||
gameState.stockPile.forEach(card => (card.faceUp = false));
|
||||
gameState.wastePile = [];
|
||||
gameState.hist.push({
|
||||
move: 'draw-reset',
|
||||
})
|
||||
}
|
||||
|
||||
if (gameState.stockPile.length > 0) {
|
||||
let cards = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (gameState.stockPile.length > 0) {
|
||||
const card = gameState.stockPile.pop();
|
||||
card.faceUp = true;
|
||||
gameState.wastePile.push(card);
|
||||
cards.push(card);
|
||||
} else {
|
||||
break; // Stop if stock runs out
|
||||
}
|
||||
}
|
||||
gameState.hist.push({
|
||||
move: "draw-3",
|
||||
cards: cards,
|
||||
});
|
||||
} else if (gameState.wastePile.length > 0) {
|
||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||
gameState.stockPile = gameState.wastePile.reverse();
|
||||
gameState.stockPile.forEach((card) => (card.faceUp = false));
|
||||
gameState.wastePile = [];
|
||||
gameState.hist.push({
|
||||
move: "draw-reset",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,8 +293,8 @@ export function draw3Cards(gameState) {
|
||||
* @returns {boolean} True if the game is won.
|
||||
*/
|
||||
export function checkWinCondition(gameState) {
|
||||
const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0);
|
||||
return foundationCardCount === 52;
|
||||
const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0);
|
||||
return foundationCardCount === 52;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,64 +303,64 @@ export function checkWinCondition(gameState) {
|
||||
* @returns {boolean} True if the game can be auto-solved.
|
||||
*/
|
||||
export function checkAutoSolve(gameState) {
|
||||
if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false;
|
||||
for (const pile of gameState.tableauPiles) {
|
||||
for (const card of pile) {
|
||||
if (!card.faceUp) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false;
|
||||
for (const pile of gameState.tableauPiles) {
|
||||
for (const card of pile) {
|
||||
if (!card.faceUp) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function autoSolveMoves(userId, gameState) {
|
||||
const moves = [];
|
||||
const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles));
|
||||
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
|
||||
const moves = [];
|
||||
const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles));
|
||||
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
|
||||
|
||||
function canMoveToFoundation(card) {
|
||||
let foundationPile = foundations.find(pile => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (!foundationPile) {
|
||||
foundationPile = foundations.find(pile => pile.length === 0);
|
||||
}
|
||||
if (foundationPile.length === 0) {
|
||||
return card.rank === 'A'; // Only Ace can be placed on empty foundation
|
||||
} else {
|
||||
const topCard = foundationPile[foundationPile.length - 1];
|
||||
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
|
||||
}
|
||||
}
|
||||
function canMoveToFoundation(card) {
|
||||
let foundationPile = foundations.find((pile) => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (!foundationPile) {
|
||||
foundationPile = foundations.find((pile) => pile.length === 0);
|
||||
}
|
||||
if (foundationPile.length === 0) {
|
||||
return card.rank === "A"; // Only Ace can be placed on empty foundation
|
||||
} else {
|
||||
const topCard = foundationPile[foundationPile.length - 1];
|
||||
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let moved;
|
||||
do {
|
||||
moved = false;
|
||||
let moved;
|
||||
do {
|
||||
moved = false;
|
||||
|
||||
for (let i = 0; i < tableau.length; i++) {
|
||||
const column = tableau[i];
|
||||
if (column.length === 0) continue;
|
||||
for (let i = 0; i < tableau.length; i++) {
|
||||
const column = tableau[i];
|
||||
if (column.length === 0) continue;
|
||||
|
||||
const card = column[column.length - 1]; // Top card of the tableau column
|
||||
let foundationIndex = foundations.findIndex(pile => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (foundationIndex === -1) {
|
||||
foundationIndex = foundations.findIndex(pile => pile.length === 0);
|
||||
}
|
||||
if(canMoveToFoundation(card)) {
|
||||
let moveData = {
|
||||
destPileIndex: foundationIndex,
|
||||
destPileType: 'foundationPiles',
|
||||
sourceCardIndex: column.length - 1,
|
||||
sourcePileIndex: i,
|
||||
sourcePileType: 'tableauPiles',
|
||||
userId: userId,
|
||||
}
|
||||
tableau[i].pop()
|
||||
foundations[foundationIndex].push(card)
|
||||
//moveCard(gameState, moveData)
|
||||
moves.push(moveData);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
} while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0));
|
||||
emitSolitaireUpdate(userId, moves)
|
||||
const card = column[column.length - 1]; // Top card of the tableau column
|
||||
let foundationIndex = foundations.findIndex((pile) => pile[pile.length - 1]?.suit === card.suit);
|
||||
if (foundationIndex === -1) {
|
||||
foundationIndex = foundations.findIndex((pile) => pile.length === 0);
|
||||
}
|
||||
if (canMoveToFoundation(card)) {
|
||||
let moveData = {
|
||||
destPileIndex: foundationIndex,
|
||||
destPileType: "foundationPiles",
|
||||
sourceCardIndex: column.length - 1,
|
||||
sourcePileIndex: i,
|
||||
sourcePileType: "tableauPiles",
|
||||
userId: userId,
|
||||
};
|
||||
tableau[i].pop();
|
||||
foundations[foundationIndex].push(card);
|
||||
//moveCard(gameState, moveData)
|
||||
moves.push(moveData);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
} while (moved); //(foundations.reduce((acc, pile) => acc + pile.length, 0));
|
||||
emitSolitaireUpdate(userId, moves);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,98 +369,99 @@ export function autoSolveMoves(userId, gameState) {
|
||||
* @param {Object} gameState - The current game state, which includes a `hist` array.
|
||||
*/
|
||||
export function undoMove(gameState) {
|
||||
if (!gameState.hist || gameState.hist.length === 0) {
|
||||
console.log("No moves to undo.");
|
||||
return; // Nothing to undo
|
||||
}
|
||||
if (!gameState.hist || gameState.hist.length === 0) {
|
||||
console.log("No moves to undo.");
|
||||
return; // Nothing to undo
|
||||
}
|
||||
|
||||
const lastMove = gameState.hist.pop(); // Get and remove the last move from history
|
||||
gameState.moves++; // Undoing a move counts as a new move
|
||||
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
|
||||
const lastMove = gameState.hist.pop(); // Get and remove the last move from history
|
||||
gameState.moves++; // Undoing a move counts as a new move
|
||||
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
|
||||
|
||||
switch (lastMove.move) {
|
||||
case 'move':
|
||||
undoCardMove(gameState, lastMove);
|
||||
break;
|
||||
case 'draw':
|
||||
undoDraw(gameState, lastMove);
|
||||
break;
|
||||
case 'draw-3':
|
||||
undoDraw3(gameState, lastMove);
|
||||
break;
|
||||
case 'draw-reset':
|
||||
undoDrawReset(gameState, lastMove);
|
||||
break;
|
||||
default:
|
||||
// If an unknown move type is found, push it back to avoid corrupting the history
|
||||
gameState.hist.push(lastMove);
|
||||
gameState.moves--; // Revert the move count increment
|
||||
gameState.score += lastMove.points || 1; // Revert the score decrement
|
||||
console.error("Unknown move type in history:", lastMove);
|
||||
break;
|
||||
}
|
||||
switch (lastMove.move) {
|
||||
case "move":
|
||||
undoCardMove(gameState, lastMove);
|
||||
break;
|
||||
case "draw":
|
||||
undoDraw(gameState, lastMove);
|
||||
break;
|
||||
case "draw-3":
|
||||
undoDraw3(gameState, lastMove);
|
||||
break;
|
||||
case "draw-reset":
|
||||
undoDrawReset(gameState, lastMove);
|
||||
break;
|
||||
default:
|
||||
// If an unknown move type is found, push it back to avoid corrupting the history
|
||||
gameState.hist.push(lastMove);
|
||||
gameState.moves--; // Revert the move count increment
|
||||
gameState.score += lastMove.points || 1; // Revert the score decrement
|
||||
console.error("Unknown move type in history:", lastMove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper functions for undoing specific moves ---
|
||||
|
||||
function undoCardMove(gameState, moveData) {
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = moveData;
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } =
|
||||
moveData;
|
||||
|
||||
// 1. Find the destination pile (where the cards are NOW)
|
||||
let currentPile;
|
||||
if (destPileType === 'tableauPiles') currentPile = gameState.tableauPiles[destPileIndex];
|
||||
else if (destPileType === 'foundationPiles') currentPile = gameState.foundationPiles[destPileIndex];
|
||||
// 1. Find the destination pile (where the cards are NOW)
|
||||
let currentPile;
|
||||
if (destPileType === "tableauPiles") currentPile = gameState.tableauPiles[destPileIndex];
|
||||
else if (destPileType === "foundationPiles") currentPile = gameState.foundationPiles[destPileIndex];
|
||||
|
||||
// 2. Remove the moved cards from their current pile
|
||||
// Using splice with a negative index removes from the end of the array
|
||||
currentPile.splice(-cardsMoved.length);
|
||||
// 2. Remove the moved cards from their current pile
|
||||
// Using splice with a negative index removes from the end of the array
|
||||
currentPile.splice(-cardsMoved.length);
|
||||
|
||||
// 3. Find the original source pile
|
||||
let originalPile;
|
||||
if (sourcePileType === 'tableauPiles') originalPile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === 'wastePile') originalPile = gameState.wastePile;
|
||||
else if (sourcePileType === 'foundationPiles') originalPile = gameState.foundationPiles[sourcePileIndex];
|
||||
// 3. Find the original source pile
|
||||
let originalPile;
|
||||
if (sourcePileType === "tableauPiles") originalPile = gameState.tableauPiles[sourcePileIndex];
|
||||
else if (sourcePileType === "wastePile") originalPile = gameState.wastePile;
|
||||
else if (sourcePileType === "foundationPiles") originalPile = gameState.foundationPiles[sourcePileIndex];
|
||||
|
||||
// 4. Put the cards back where they came from
|
||||
// Using splice to insert the cards back at their original index
|
||||
originalPile.splice(sourceCardIndex, 0, ...cardsMoved);
|
||||
// 4. Put the cards back where they came from
|
||||
// Using splice to insert the cards back at their original index
|
||||
originalPile.splice(sourceCardIndex, 0, ...cardsMoved);
|
||||
|
||||
// 5. If a card was flipped during the move, flip it back to face-down
|
||||
if (cardWasFlipped) {
|
||||
const cardToUnflip = originalPile[sourceCardIndex - 1];
|
||||
if (cardToUnflip) {
|
||||
cardToUnflip.faceUp = false;
|
||||
}
|
||||
}
|
||||
// 5. If a card was flipped during the move, flip it back to face-down
|
||||
if (cardWasFlipped) {
|
||||
const cardToUnflip = originalPile[sourceCardIndex - 1];
|
||||
if (cardToUnflip) {
|
||||
cardToUnflip.faceUp = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function undoDraw(gameState, moveData) {
|
||||
// A 'draw' move means a card went from stock to waste.
|
||||
// To undo, move it from waste back to stock and flip it face-down.
|
||||
const cardToReturn = gameState.wastePile.pop();
|
||||
if (cardToReturn) {
|
||||
cardToReturn.faceUp = false;
|
||||
gameState.stockPile.push(cardToReturn);
|
||||
}
|
||||
// A 'draw' move means a card went from stock to waste.
|
||||
// To undo, move it from waste back to stock and flip it face-down.
|
||||
const cardToReturn = gameState.wastePile.pop();
|
||||
if (cardToReturn) {
|
||||
cardToReturn.faceUp = false;
|
||||
gameState.stockPile.push(cardToReturn);
|
||||
}
|
||||
}
|
||||
|
||||
function undoDraw3(gameState, moveData) {
|
||||
// A 'draw-3' move means up to 3 cards went from stock to
|
||||
// waste. To undo, move them back to stock and flip them face-down.
|
||||
const cardsToReturn = moveData.cards || [];
|
||||
for (let i = 0; i < cardsToReturn.length; i++) {
|
||||
const card = gameState.wastePile.pop();
|
||||
if (card) {
|
||||
card.faceUp = false;
|
||||
gameState.stockPile.push(card);
|
||||
}
|
||||
}
|
||||
// A 'draw-3' move means up to 3 cards went from stock to
|
||||
// waste. To undo, move them back to stock and flip them face-down.
|
||||
const cardsToReturn = moveData.cards || [];
|
||||
for (let i = 0; i < cardsToReturn.length; i++) {
|
||||
const card = gameState.wastePile.pop();
|
||||
if (card) {
|
||||
card.faceUp = false;
|
||||
gameState.stockPile.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function undoDrawReset(gameState, moveData) {
|
||||
// A 'draw-reset' means the waste pile was moved to the stock pile.
|
||||
// To undo, move the stock pile back to the waste pile and flip cards face-up.
|
||||
gameState.wastePile = gameState.stockPile.reverse();
|
||||
gameState.wastePile.forEach(card => (card.faceUp = true));
|
||||
gameState.stockPile = [];
|
||||
// A 'draw-reset' means the waste pile was moved to the stock pile.
|
||||
// To undo, move the stock pile back to the waste pile and flip cards face-up.
|
||||
gameState.wastePile = gameState.stockPile.reverse();
|
||||
gameState.wastePile.forEach((card) => (card.faceUp = true));
|
||||
gameState.stockPile = [];
|
||||
}
|
||||
@@ -44,7 +44,6 @@ export let activePredis = {};
|
||||
// Format: { [userId]: { endAt, lastMessage } }
|
||||
export let activeSlowmodes = {};
|
||||
|
||||
|
||||
// --- Queues for Matchmaking ---
|
||||
|
||||
// Stores user IDs waiting to play Tic-Tac-Toe.
|
||||
@@ -55,7 +54,6 @@ export let connect4Queue = [];
|
||||
|
||||
export let queueMessagesEndpoints = [];
|
||||
|
||||
|
||||
// --- Rate Limiting and Caching ---
|
||||
|
||||
// Tracks message timestamps for the channel points system, keyed by user ID.
|
||||
|
||||
@@ -4,19 +4,19 @@ export const C4_COLS = 7;
|
||||
|
||||
// A predefined list of choices for the /timeout command's duration option.
|
||||
const TimesChoices = [
|
||||
{ name: '1 minute', value: 60 },
|
||||
{ name: '5 minutes', value: 300 },
|
||||
{ name: '10 minutes', value: 600 },
|
||||
{ name: '15 minutes', value: 900 },
|
||||
{ name: '30 minutes', value: 1800 },
|
||||
{ name: '1 heure', value: 3600 },
|
||||
{ name: '2 heures', value: 7200 },
|
||||
{ name: '3 heures', value: 10800 },
|
||||
{ name: '6 heures', value: 21600 },
|
||||
{ name: '9 heures', value: 32400 },
|
||||
{ name: '12 heures', value: 43200 },
|
||||
{ name: '16 heures', value: 57600 },
|
||||
{ name: '1 jour', value: 86400 },
|
||||
{ name: "1 minute", value: 60 },
|
||||
{ name: "5 minutes", value: 300 },
|
||||
{ name: "10 minutes", value: 600 },
|
||||
{ name: "15 minutes", value: 900 },
|
||||
{ name: "30 minutes", value: 1800 },
|
||||
{ name: "1 heure", value: 3600 },
|
||||
{ name: "2 heures", value: 7200 },
|
||||
{ name: "3 heures", value: 10800 },
|
||||
{ name: "6 heures", value: 21600 },
|
||||
{ name: "9 heures", value: 32400 },
|
||||
{ name: "12 heures", value: 43200 },
|
||||
{ name: "16 heures", value: 57600 },
|
||||
{ name: "1 jour", value: 86400 },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -24,10 +24,9 @@ const TimesChoices = [
|
||||
* @returns {Array<object>} The array of time choices.
|
||||
*/
|
||||
export function getTimesChoices() {
|
||||
return TimesChoices;
|
||||
return TimesChoices;
|
||||
}
|
||||
|
||||
|
||||
// --- Connect 4 Logic ---
|
||||
|
||||
/**
|
||||
@@ -35,7 +34,9 @@ export function getTimesChoices() {
|
||||
* @returns {Array<Array<null>>} A 2D array representing the board.
|
||||
*/
|
||||
export function createConnect4Board() {
|
||||
return Array(C4_ROWS).fill(null).map(() => Array(C4_COLS).fill(null));
|
||||
return Array(C4_ROWS)
|
||||
.fill(null)
|
||||
.map(() => Array(C4_COLS).fill(null));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,43 +46,95 @@ export function createConnect4Board() {
|
||||
* @returns {object} An object with `win` (boolean) and `pieces` (array of winning piece coordinates).
|
||||
*/
|
||||
export function checkConnect4Win(board, player) {
|
||||
// Check horizontal
|
||||
for (let r = 0; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r][c+1] === player && board[r][c+2] === player && board[r][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check horizontal
|
||||
for (let r = 0; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r][c + 1] === player &&
|
||||
board[r][c + 2] === player &&
|
||||
board[r][c + 3] === player
|
||||
) {
|
||||
return {
|
||||
win: true,
|
||||
pieces: [
|
||||
{ row: r, col: c },
|
||||
{ row: r, col: c + 1 },
|
||||
{ row: r, col: c + 2 },
|
||||
{ row: r, col: c + 3 },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vertical
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c < C4_COLS; c++) {
|
||||
if (board[r][c] === player && board[r+1][c] === player && board[r+2][c] === player && board[r+3][c] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check vertical
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c < C4_COLS; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r + 1][c] === player &&
|
||||
board[r + 2][c] === player &&
|
||||
board[r + 3][c] === player
|
||||
) {
|
||||
return {
|
||||
win: true,
|
||||
pieces: [
|
||||
{ row: r, col: c },
|
||||
{ row: r + 1, col: c },
|
||||
{ row: r + 2, col: c },
|
||||
{ row: r + 3, col: c },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (down-right)
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r+1][c+1] === player && board[r+2][c+2] === player && board[r+3][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c+1}, {row:r+2, col:c+2}, {row:r+3, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check diagonal (down-right)
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r + 1][c + 1] === player &&
|
||||
board[r + 2][c + 2] === player &&
|
||||
board[r + 3][c + 3] === player
|
||||
) {
|
||||
return {
|
||||
win: true,
|
||||
pieces: [
|
||||
{ row: r, col: c },
|
||||
{ row: r + 1, col: c + 1 },
|
||||
{ row: r + 2, col: c + 2 },
|
||||
{ row: r + 3, col: c + 3 },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (up-right)
|
||||
for (let r = 3; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r-1][c+1] === player && board[r-2][c+2] === player && board[r-3][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r-1, col:c+1}, {row:r-2, col:c+2}, {row:r-3, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check diagonal (up-right)
|
||||
for (let r = 3; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r - 1][c + 1] === player &&
|
||||
board[r - 2][c + 2] === player &&
|
||||
board[r - 3][c + 3] === player
|
||||
) {
|
||||
return {
|
||||
win: true,
|
||||
pieces: [
|
||||
{ row: r, col: c },
|
||||
{ row: r - 1, col: c + 1 },
|
||||
{ row: r - 2, col: c + 2 },
|
||||
{ row: r - 3, col: c + 3 },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { win: false, pieces: [] };
|
||||
return { win: false, pieces: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +143,8 @@ export function checkConnect4Win(board, player) {
|
||||
* @returns {boolean} True if the game is a draw.
|
||||
*/
|
||||
export function checkConnect4Draw(board) {
|
||||
// A draw occurs if the top row is completely full.
|
||||
return board[0].every(cell => cell !== null);
|
||||
// A draw occurs if the top row is completely full.
|
||||
return board[0].every((cell) => cell !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,10 +153,10 @@ export function checkConnect4Draw(board) {
|
||||
* @returns {string} The formatted string representation of the board.
|
||||
*/
|
||||
export function formatConnect4BoardForDiscord(board) {
|
||||
const symbols = {
|
||||
'R': '🔴',
|
||||
'Y': '🟡',
|
||||
null: '⚪' // Using a white circle for empty slots
|
||||
};
|
||||
return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n');
|
||||
const symbols = {
|
||||
R: "🔴",
|
||||
Y: "🟡",
|
||||
null: "⚪", // Using a white circle for empty slots
|
||||
};
|
||||
return board.map((row) => row.map((cell) => symbols[cell]).join("")).join("\n");
|
||||
}
|
||||
@@ -1,60 +1,65 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import { verifyKeyMiddleware } from 'discord-interactions';
|
||||
import { handleInteraction } from '../bot/handlers/interactionCreate.js';
|
||||
import { client } from '../bot/client.js';
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { verifyKeyMiddleware } from "discord-interactions";
|
||||
import { handleInteraction } from "../bot/handlers/interactionCreate.js";
|
||||
import { client } from "../bot/client.js";
|
||||
|
||||
// Import route handlers
|
||||
import { apiRoutes } from './routes/api.js';
|
||||
import { pokerRoutes } from './routes/poker.js';
|
||||
import { solitaireRoutes } from './routes/solitaire.js';
|
||||
import {getSocketIo} from "./socket.js";
|
||||
import {erinyesRoutes} from "./routes/erinyes.js";
|
||||
import {blackjackRoutes} from "./routes/blackjack.js";
|
||||
import { apiRoutes } from "./routes/api.js";
|
||||
import { pokerRoutes } from "./routes/poker.js";
|
||||
import { solitaireRoutes } from "./routes/solitaire.js";
|
||||
import { getSocketIo } from "./socket.js";
|
||||
import { blackjackRoutes } from "./routes/blackjack.js";
|
||||
import { marketRoutes } from "./routes/market.js";
|
||||
|
||||
// --- EXPRESS APP INITIALIZATION ---
|
||||
const app = express();
|
||||
const io = getSocketIo();
|
||||
const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||
|
||||
// --- GLOBAL MIDDLEWARE ---
|
||||
|
||||
// CORS Middleware
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', FLAPI_URL);
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires');
|
||||
next();
|
||||
res.header("Access-Control-Allow-Origin", FLAPI_URL);
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
|
||||
);
|
||||
next();
|
||||
});
|
||||
|
||||
// --- PRIMARY DISCORD INTERACTION ENDPOINT ---
|
||||
// This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.)
|
||||
app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
|
||||
// The actual logic is delegated to a dedicated handler for better organization
|
||||
await handleInteraction(req, res, client);
|
||||
app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
|
||||
// The actual logic is delegated to a dedicated handler for better organization
|
||||
await handleInteraction(req, res, client);
|
||||
});
|
||||
|
||||
// JSON Body Parser Middleware
|
||||
app.use(express.json());
|
||||
|
||||
// --- STATIC ASSETS ---
|
||||
app.use('/public', express.static('public'));
|
||||
|
||||
app.use("/public", express.static("public"));
|
||||
|
||||
// --- API ROUTES ---
|
||||
|
||||
// General API routes (users, polls, etc.)
|
||||
app.use('/api', apiRoutes(client, io));
|
||||
app.use("/api", apiRoutes(client, io));
|
||||
|
||||
// Poker-specific routes
|
||||
app.use('/api/poker', pokerRoutes(client, io));
|
||||
app.use("/api/poker", pokerRoutes(client, io));
|
||||
|
||||
// Solitaire-specific routes
|
||||
app.use('/api/solitaire', solitaireRoutes(client, io));
|
||||
app.use("/api/solitaire", solitaireRoutes(client, io));
|
||||
|
||||
app.use('/api/blackjack', blackjackRoutes(client, io));
|
||||
// Blackjack-specific routes
|
||||
app.use("/api/blackjack", blackjackRoutes(client, io));
|
||||
|
||||
// Market-specific routes
|
||||
app.use("/api/market-place", marketRoutes(client, io));
|
||||
|
||||
// erinyes-specific routes
|
||||
app.use('/api/erinyes', erinyesRoutes(client, io));
|
||||
|
||||
// app.use("/api/erinyes", erinyesRoutes(client, io));
|
||||
|
||||
export { app };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,342 +1,364 @@
|
||||
// /routes/blackjack.js
|
||||
import express from "express";
|
||||
import {
|
||||
createBlackjackRoom,
|
||||
startBetting,
|
||||
dealInitial,
|
||||
autoActions,
|
||||
everyoneDone,
|
||||
dealerPlay,
|
||||
settleAll,
|
||||
applyAction,
|
||||
publicPlayerView,
|
||||
handValue,
|
||||
dealerShouldHit, draw
|
||||
createBlackjackRoom,
|
||||
startBetting,
|
||||
dealInitial,
|
||||
autoActions,
|
||||
everyoneDone,
|
||||
dealerPlay,
|
||||
settleAll,
|
||||
applyAction,
|
||||
publicPlayerView,
|
||||
handValue,
|
||||
dealerShouldHit,
|
||||
draw,
|
||||
} from "../../game/blackjack.js";
|
||||
|
||||
// Optional: hook into your DB & Discord systems if available
|
||||
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
||||
import { client } from "../../bot/client.js";
|
||||
import {emitToast, emitUpdate} from "../socket.js";
|
||||
import {EmbedBuilder} from "discord.js";
|
||||
import { emitToast, emitUpdate } from "../socket.js";
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
|
||||
export function blackjackRoutes(io) {
|
||||
const router = express.Router();
|
||||
const router = express.Router();
|
||||
|
||||
// --- Singleton continuous room ---
|
||||
const room = createBlackjackRoom({
|
||||
minBet: 10,
|
||||
maxBet: 10000,
|
||||
fakeMoney: false,
|
||||
decks: 6,
|
||||
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
||||
blackjackPayout: 1.5, // 3:2
|
||||
cutCardRatio: 0.25,
|
||||
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 7000 },
|
||||
animation: { dealerDrawMs: 1000 }
|
||||
});
|
||||
// --- Singleton continuous room ---
|
||||
const room = createBlackjackRoom({
|
||||
minBet: 10,
|
||||
maxBet: 10000,
|
||||
fakeMoney: false,
|
||||
decks: 6,
|
||||
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
||||
blackjackPayout: 1.5, // 3:2
|
||||
cutCardRatio: 0.25,
|
||||
phaseDurations: {
|
||||
bettingMs: 10000,
|
||||
dealMs: 2000,
|
||||
playMsPerPlayer: 20000,
|
||||
revealMs: 1000,
|
||||
payoutMs: 7000,
|
||||
},
|
||||
animation: { dealerDrawMs: 1000 },
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
||||
let animatingDealer = false;
|
||||
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
let animatingDealer = false;
|
||||
|
||||
async function runDealerAnimation() {
|
||||
if (animatingDealer) return;
|
||||
animatingDealer = true;
|
||||
async function runDealerAnimation() {
|
||||
if (animatingDealer) return;
|
||||
animatingDealer = true;
|
||||
|
||||
room.status = "dealer";
|
||||
room.dealer.holeHidden = false;
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000);
|
||||
emitUpdate("dealer-reveal", snapshot(room));
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
room.status = "dealer";
|
||||
room.dealer.holeHidden = false;
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000);
|
||||
emitUpdate("dealer-reveal", snapshot(room));
|
||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||
|
||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||
room.dealer.cards.push(draw(room.shoe));
|
||||
room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500);
|
||||
emitUpdate("dealer-hit", snapshot(room));
|
||||
await sleep(room.settings.animation?.dealerDrawMs ?? 500);
|
||||
}
|
||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||
room.dealer.cards.push(draw(room.shoe));
|
||||
room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500);
|
||||
emitUpdate("dealer-hit", snapshot(room));
|
||||
await sleep(room.settings.animation?.dealerDrawMs ?? 500);
|
||||
}
|
||||
|
||||
settleAll(room);
|
||||
room.status = "payout";
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
|
||||
emitUpdate("payout", snapshot(room))
|
||||
settleAll(room);
|
||||
room.status = "payout";
|
||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
|
||||
emitUpdate("payout", snapshot(room));
|
||||
|
||||
animatingDealer = false;
|
||||
}
|
||||
animatingDealer = false;
|
||||
}
|
||||
|
||||
function autoTimeoutAFK(now) {
|
||||
if (room.status !== "playing") return false;
|
||||
if (!room.phase_ends_at || now < room.phase_ends_at) return false;
|
||||
function autoTimeoutAFK(now) {
|
||||
if (room.status !== "playing") return false;
|
||||
if (!room.phase_ends_at || now < room.phase_ends_at) return false;
|
||||
|
||||
let changed = false;
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
||||
h.surrendered = true;
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-timeout", userId: p.id });
|
||||
changed = true;
|
||||
} else if (h.hasActed && !h.stood) {
|
||||
h.stood = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||
return changed;
|
||||
}
|
||||
let changed = false;
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
||||
h.surrendered = true;
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-timeout", userId: p.id });
|
||||
changed = true;
|
||||
} else if (h.hasActed && !h.stood) {
|
||||
h.stood = true;
|
||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||
return changed;
|
||||
}
|
||||
|
||||
function snapshot(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
status: r.status,
|
||||
phase_ends_at: r.phase_ends_at,
|
||||
minBet: r.minBet,
|
||||
maxBet: r.maxBet,
|
||||
settings: r.settings,
|
||||
dealer: { cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total },
|
||||
players: Object.values(r.players).map(publicPlayerView),
|
||||
shoeCount: r.shoe.length,
|
||||
};
|
||||
}
|
||||
function snapshot(r) {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
status: r.status,
|
||||
phase_ends_at: r.phase_ends_at,
|
||||
minBet: r.minBet,
|
||||
maxBet: r.maxBet,
|
||||
settings: r.settings,
|
||||
dealer: {
|
||||
cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards,
|
||||
total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total,
|
||||
},
|
||||
players: Object.values(r.players).map(publicPlayerView),
|
||||
shoeCount: r.shoe.length,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Public endpoints ---
|
||||
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
|
||||
// --- Public endpoints ---
|
||||
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
|
||||
|
||||
router.post("/join", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId) return res.status(400).json({ message: "userId required" });
|
||||
if (room.players[userId]) return res.status(200).json({ message: "Already here" });
|
||||
router.post("/join", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId) return res.status(400).json({ message: "userId required" });
|
||||
if (room.players[userId]) return res.status(200).json({ message: "Already here" });
|
||||
|
||||
const user = await client.users.fetch(userId);
|
||||
const bank = getUser.get(userId)?.coins ?? 0;
|
||||
const user = await client.users.fetch(userId);
|
||||
const bank = getUser.get(userId)?.coins ?? 0;
|
||||
|
||||
room.players[userId] = {
|
||||
id: userId,
|
||||
globalName: user.globalName || user.username,
|
||||
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
bank,
|
||||
currentBet: 0,
|
||||
inRound: false,
|
||||
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 }],
|
||||
activeHand: 0,
|
||||
joined_at: Date.now(),
|
||||
msgId: null,
|
||||
totalDelta: 0,
|
||||
totalBets: 0,
|
||||
};
|
||||
room.players[userId] = {
|
||||
id: userId,
|
||||
globalName: user.globalName || user.username,
|
||||
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
bank,
|
||||
currentBet: 0,
|
||||
inRound: false,
|
||||
hands: [
|
||||
{
|
||||
cards: [],
|
||||
stood: false,
|
||||
busted: false,
|
||||
doubled: false,
|
||||
surrendered: false,
|
||||
hasActed: false,
|
||||
bet: 0,
|
||||
},
|
||||
],
|
||||
activeHand: 0,
|
||||
joined_at: Date.now(),
|
||||
msgId: null,
|
||||
totalDelta: 0,
|
||||
totalBets: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> joue au Blackjack`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor('#5865f2')
|
||||
.setTimestamp(new Date());
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> joue au Blackjack`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true,
|
||||
},
|
||||
)
|
||||
.setColor("#5865f2")
|
||||
.setTimestamp(new Date());
|
||||
|
||||
const msg = await generalChannel.send({ embeds: [embed] });
|
||||
room.players[userId].msgId = msg.id;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
const msg = await generalChannel.send({ embeds: [embed] });
|
||||
room.players[userId].msgId = msg.id;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
emitUpdate("player-joined", snapshot(room));
|
||||
return res.status(200).json({ message: "joined" });
|
||||
});
|
||||
emitUpdate("player-joined", snapshot(room));
|
||||
return res.status(200).json({ message: "joined" });
|
||||
});
|
||||
|
||||
router.post("/leave", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" });
|
||||
router.post("/leave", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" });
|
||||
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> a quitté la table de Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setColor(room.players[userId].totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> a quitté la table de Blackjack.`)
|
||||
.addFields(
|
||||
{
|
||||
name: `Gains`,
|
||||
value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `Mises jouées`,
|
||||
value: `**${room.players[userId].totalBets}**`,
|
||||
inline: true,
|
||||
},
|
||||
)
|
||||
.setColor(room.players[userId].totalDelta >= 0 ? 0x22a55b : 0xed4245)
|
||||
.setTimestamp(new Date());
|
||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
const p = room.players[userId];
|
||||
if (p.inRound) {
|
||||
// leave after round to avoid abandoning an active bet
|
||||
room.leavingAfterRound[userId] = true;
|
||||
return res.status(200).json({ message: "will-leave-after-round" });
|
||||
} else {
|
||||
delete room.players[userId];
|
||||
emitUpdate("player-left", snapshot(room));
|
||||
return res.status(200).json({ message: "left" });
|
||||
}
|
||||
});
|
||||
const p = room.players[userId];
|
||||
if (p.inRound) {
|
||||
// leave after round to avoid abandoning an active bet
|
||||
room.leavingAfterRound[userId] = true;
|
||||
return res.status(200).json({ message: "will-leave-after-round" });
|
||||
} else {
|
||||
delete room.players[userId];
|
||||
emitUpdate("player-left", snapshot(room));
|
||||
return res.status(200).json({ message: "left" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/bet", (req, res) => {
|
||||
const { userId, amount } = req.body;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
|
||||
router.post("/bet", (req, res) => {
|
||||
const { userId, amount } = req.body;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
|
||||
|
||||
const bet = Math.floor(Number(amount) || 0);
|
||||
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
|
||||
const bet = Math.floor(Number(amount) || 0);
|
||||
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
|
||||
|
||||
if (!room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'BLACKJACK_BET',
|
||||
coins_amount: -bet, user_new_amount: coins - bet,
|
||||
});
|
||||
p.bank = coins - bet;
|
||||
}
|
||||
if (!room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId,
|
||||
target_user_id: null,
|
||||
action: "BLACKJACK_BET",
|
||||
coins_amount: -bet,
|
||||
user_new_amount: coins - bet,
|
||||
});
|
||||
p.bank = coins - bet;
|
||||
}
|
||||
|
||||
p.currentBet = bet;
|
||||
p.hands[p.activeHand].bet = bet;
|
||||
emitToast({ type: "player-bet", userId, amount: bet });
|
||||
emitUpdate("bet-placed", snapshot(room));
|
||||
return res.status(200).json({ message: "bet-accepted" });
|
||||
});
|
||||
p.currentBet = bet;
|
||||
p.hands[p.activeHand].bet = bet;
|
||||
emitToast({ type: "player-bet", userId, amount: bet });
|
||||
emitUpdate("bet-placed", snapshot(room));
|
||||
return res.status(200).json({ message: "bet-accepted" });
|
||||
});
|
||||
|
||||
router.post("/action/:action", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const action = req.params.action;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" });
|
||||
router.post("/action/:action", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const action = req.params.action;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" });
|
||||
|
||||
// Handle extra coin lock for double
|
||||
if (action === "double" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'BLACKJACK_DOUBLE',
|
||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
// Handle extra coin lock for double
|
||||
if (action === "double" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId,
|
||||
target_user_id: null,
|
||||
action: "BLACKJACK_DOUBLE",
|
||||
coins_amount: -hand.bet,
|
||||
user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
|
||||
if (action === "split" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'BLACKJACK_SPLIT',
|
||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
if (action === "split" && !room.settings.fakeMoney) {
|
||||
const userDB = getUser.get(userId);
|
||||
const coins = userDB?.coins ?? 0;
|
||||
const hand = p.hands[p.activeHand];
|
||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
|
||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||
insertLog.run({
|
||||
id: `${userId}-blackjack-${Date.now()}`,
|
||||
user_id: userId,
|
||||
target_user_id: null,
|
||||
action: "BLACKJACK_SPLIT",
|
||||
coins_amount: -hand.bet,
|
||||
user_new_amount: coins - hand.bet,
|
||||
});
|
||||
p.bank = coins - hand.bet;
|
||||
// effective bet size is handled in settlement via hand.doubled flag
|
||||
}
|
||||
|
||||
try {
|
||||
const evt = applyAction(room, userId, action);
|
||||
emitToast({ type: `player-${evt}`, userId });
|
||||
emitUpdate("player-action", snapshot(room));
|
||||
return res.status(200).json({ message: "ok" });
|
||||
} catch (e) {
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
});
|
||||
try {
|
||||
const evt = applyAction(room, userId, action);
|
||||
emitToast({ type: `player-${evt}`, userId });
|
||||
emitUpdate("player-action", snapshot(room));
|
||||
return res.status(200).json({ message: "ok" });
|
||||
} catch (e) {
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Game loop ---
|
||||
// Simple phase machine that runs regardless of player count.
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
// --- Game loop ---
|
||||
// Simple phase machine that runs regardless of player count.
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
|
||||
if (room.status === "betting" && now >= room.phase_ends_at) {
|
||||
const hasBets = Object.values(room.players).some(p => p.currentBet >= room.minBet);
|
||||
if (!hasBets) {
|
||||
// Extend betting window if no one bet
|
||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||
emitUpdate("betting-extend", snapshot(room));
|
||||
return;
|
||||
}
|
||||
dealInitial(room);
|
||||
autoActions(room);
|
||||
emitUpdate("initial-deal", snapshot(room));
|
||||
if (room.status === "betting" && now >= room.phase_ends_at) {
|
||||
const hasBets = Object.values(room.players).some((p) => p.currentBet >= room.minBet);
|
||||
if (!hasBets) {
|
||||
// Extend betting window if no one bet
|
||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||
emitUpdate("betting-extend", snapshot(room));
|
||||
return;
|
||||
}
|
||||
dealInitial(room);
|
||||
autoActions(room);
|
||||
emitUpdate("initial-deal", snapshot(room));
|
||||
|
||||
room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer;
|
||||
emitUpdate("playing-start", snapshot(room));
|
||||
return;
|
||||
}
|
||||
room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer;
|
||||
emitUpdate("playing-start", snapshot(room));
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.status === "playing") {
|
||||
// If the per-round playing timer expired, auto-surrender AFKs (you already added this)
|
||||
if (room.phase_ends_at && now >= room.phase_ends_at) {
|
||||
autoTimeoutAFK(now);
|
||||
}
|
||||
if (room.status === "playing") {
|
||||
// If the per-round playing timer expired, auto-surrender AFKs (you already added this)
|
||||
if (room.phase_ends_at && now >= room.phase_ends_at) {
|
||||
autoTimeoutAFK(now);
|
||||
}
|
||||
|
||||
// Everyone acted before the timer? Cut short and go straight to dealer.
|
||||
if (everyoneDone(room) && !animatingDealer) {
|
||||
// Set a new server-driven deadline for the reveal pause,
|
||||
// so the client's countdown immediately reflects the phase change.
|
||||
room.phase_ends_at = Date.now();
|
||||
emitUpdate("playing-cut-short", snapshot(room));
|
||||
// Everyone acted before the timer? Cut short and go straight to dealer.
|
||||
if (everyoneDone(room) && !animatingDealer) {
|
||||
// Set a new server-driven deadline for the reveal pause,
|
||||
// so the client's countdown immediately reflects the phase change.
|
||||
room.phase_ends_at = Date.now();
|
||||
emitUpdate("playing-cut-short", snapshot(room));
|
||||
|
||||
// Now run the animated dealer with per-step updates
|
||||
runDealerAnimation();
|
||||
}
|
||||
}
|
||||
// Now run the animated dealer with per-step updates
|
||||
runDealerAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
if (room.status === "payout" && now >= room.phase_ends_at) {
|
||||
// Remove leavers
|
||||
for (const userId of Object.keys(room.leavingAfterRound)) {
|
||||
delete room.players[userId];
|
||||
}
|
||||
// Prepare next round
|
||||
startBetting(room, now);
|
||||
emitUpdate("new-round", snapshot(room));
|
||||
}
|
||||
}, 100);
|
||||
if (room.status === "payout" && now >= room.phase_ends_at) {
|
||||
// Remove leavers
|
||||
for (const userId of Object.keys(room.leavingAfterRound)) {
|
||||
delete room.players[userId];
|
||||
}
|
||||
// Prepare next round
|
||||
startBetting(room, now);
|
||||
emitUpdate("new-round", snapshot(room));
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return router;
|
||||
return router;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {erinyesRooms} from "../../game/state.js";
|
||||
import {socketEmit} from "../socket.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { erinyesRooms } from "../../game/state.js";
|
||||
import { socketEmit } from "../socket.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,89 +12,91 @@ const router = express.Router();
|
||||
* @returns {object} The configured Express router.
|
||||
*/
|
||||
export function erinyesRoutes(client, io) {
|
||||
// --- Router Management Endpoints
|
||||
|
||||
// --- Router Management Endpoints
|
||||
router.get("/", (req, res) => {
|
||||
res.status(200).json({ rooms: erinyesRooms });
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.status(200).json({ rooms: erinyesRooms })
|
||||
})
|
||||
router.get("/:id", (req, res) => {
|
||||
const room = erinyesRooms[req.params.id];
|
||||
if (room) {
|
||||
res.status(200).json({ room });
|
||||
} else {
|
||||
res.status(404).json({ message: "Room not found." });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
const room = erinyesRooms[req.params.id];
|
||||
if (room) {
|
||||
res.status(200).json({ room });
|
||||
} else {
|
||||
res.status(404).json({ message: 'Room not found.' });
|
||||
}
|
||||
})
|
||||
router.post("/create", async (req, res) => {
|
||||
const { creatorId } = req.body;
|
||||
if (!creatorId) return res.status(404).json({ message: "Creator ID is required." });
|
||||
|
||||
router.post('/create', async (req, res) => {
|
||||
const { creatorId } = req.body;
|
||||
if (!creatorId) return res.status(404).json({ message: 'Creator ID is required.' });
|
||||
if (Object.values(erinyesRooms).some((room) => creatorId === room.host_id || room.players[creatorId])) {
|
||||
res.status(404).json({ message: "You are already in a room." });
|
||||
}
|
||||
|
||||
if (Object.values(erinyesRooms).some(room => creatorId === room.host_id || room.players[creatorId])) {
|
||||
res.status(404).json({ message: 'You are already in a room.' });
|
||||
}
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const id = uuidv4();
|
||||
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const id = uuidv4()
|
||||
createRoom({
|
||||
host_id: creatorId,
|
||||
host_name: creator.globalName,
|
||||
game_rules: {}, // Specific game rules
|
||||
roles: [], // Every role in the game
|
||||
});
|
||||
|
||||
createRoom({
|
||||
host_id: creatorId,
|
||||
host_name: creator.globalName,
|
||||
game_rules: {}, // Specific game rules
|
||||
roles: [], // Every role in the game
|
||||
})
|
||||
await socketEmit("erinyes-update", {
|
||||
room: erinyesRooms[id],
|
||||
type: "room-created",
|
||||
});
|
||||
res.status(200).json({ room: id });
|
||||
});
|
||||
|
||||
await socketEmit('erinyes-update', { room: erinyesRooms[id], type: 'room-created' });
|
||||
res.status(200).json({ room: id });
|
||||
})
|
||||
|
||||
return router;
|
||||
return router;
|
||||
}
|
||||
|
||||
function createRoom(config) {
|
||||
erinyesRooms[config.id] = {
|
||||
host_id: config.host_id,
|
||||
host_name: config.host_name,
|
||||
created_at: Date.now(),
|
||||
last_move_at: null,
|
||||
players: {},
|
||||
current_player: null,
|
||||
current_turn: null,
|
||||
playing: false,
|
||||
game_rules: createGameRules(config.game_rules),
|
||||
roles: config.roles,
|
||||
roles_rules: createRolesRules(config.roles),
|
||||
bonuses: {}
|
||||
}
|
||||
erinyesRooms[config.id] = {
|
||||
host_id: config.host_id,
|
||||
host_name: config.host_name,
|
||||
created_at: Date.now(),
|
||||
last_move_at: null,
|
||||
players: {},
|
||||
current_player: null,
|
||||
current_turn: null,
|
||||
playing: false,
|
||||
game_rules: createGameRules(config.game_rules),
|
||||
roles: config.roles,
|
||||
roles_rules: createRolesRules(config.roles),
|
||||
bonuses: {},
|
||||
};
|
||||
}
|
||||
|
||||
function createGameRules(config) {
|
||||
return {
|
||||
day_vote_time: config.day_vote_time ?? 60000,
|
||||
// ...
|
||||
};
|
||||
return {
|
||||
day_vote_time: config.day_vote_time ?? 60000,
|
||||
// ...
|
||||
};
|
||||
}
|
||||
|
||||
function createRolesRules(roles) {
|
||||
const roles_rules = {}
|
||||
const roles_rules = {};
|
||||
|
||||
roles.forEach(role => {
|
||||
switch (role) {
|
||||
case 'erynie':
|
||||
roles_rules[role] = {
|
||||
//...
|
||||
};
|
||||
break;
|
||||
//...
|
||||
default:
|
||||
roles_rules[role] = {
|
||||
//...
|
||||
};
|
||||
break;
|
||||
}
|
||||
})
|
||||
roles.forEach((role) => {
|
||||
switch (role) {
|
||||
case "erynie":
|
||||
roles_rules[role] = {
|
||||
//...
|
||||
};
|
||||
break;
|
||||
//...
|
||||
default:
|
||||
roles_rules[role] = {
|
||||
//...
|
||||
};
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return roles_rules;
|
||||
return roles_rules;
|
||||
}
|
||||
73
src/server/routes/market.js
Normal file
73
src/server/routes/market.js
Normal 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;
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
import express from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { uniqueNamesGenerator, adjectives } from 'unique-names-generator';
|
||||
import pkg from 'pokersolver';
|
||||
import express from "express";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { uniqueNamesGenerator, adjectives } from "unique-names-generator";
|
||||
import pkg from "pokersolver";
|
||||
const { Hand } = pkg;
|
||||
|
||||
import { pokerRooms } from '../../game/state.js';
|
||||
import { initialShuffledCards, getFirstActivePlayerAfterDealer, getNextActivePlayer, checkEndOfBettingRound, checkRoomWinners } from '../../game/poker.js';
|
||||
import { pokerEloHandler } from '../../game/elo.js';
|
||||
import { getUser, updateUserCoins, insertLog } from '../../database/index.js';
|
||||
import { pokerRooms } from "../../game/state.js";
|
||||
import {
|
||||
initialShuffledCards,
|
||||
getFirstActivePlayerAfterDealer,
|
||||
getNextActivePlayer,
|
||||
checkEndOfBettingRound,
|
||||
checkRoomWinners,
|
||||
} from "../../game/poker.js";
|
||||
import { pokerEloHandler } from "../../game/elo.js";
|
||||
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
||||
import { sleep } from "openai/core";
|
||||
import {client} from "../../bot/client.js";
|
||||
import {emitPokerToast, emitPokerUpdate} from "../socket.js";
|
||||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
||||
import {formatAmount} from "../../utils/index.js";
|
||||
import { client } from "../../bot/client.js";
|
||||
import { emitPokerToast, emitPokerUpdate } from "../socket.js";
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||
import { formatAmount } from "../../utils/index.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -23,474 +29,540 @@ const router = express.Router();
|
||||
* @returns {object} The configured Express router.
|
||||
*/
|
||||
export function pokerRoutes(client, io) {
|
||||
// --- Room Management Endpoints ---
|
||||
|
||||
// --- Room Management Endpoints ---
|
||||
router.get("/", (req, res) => {
|
||||
res.status(200).json({ rooms: pokerRooms });
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.status(200).json({ rooms: pokerRooms });
|
||||
});
|
||||
router.get("/:id", (req, res) => {
|
||||
const room = pokerRooms[req.params.id];
|
||||
if (room) {
|
||||
res.status(200).json({ room });
|
||||
} else {
|
||||
res.status(404).json({ message: "Poker room not found." });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', (req, res) => {
|
||||
const room = pokerRooms[req.params.id];
|
||||
if (room) {
|
||||
res.status(200).json({ room });
|
||||
} else {
|
||||
res.status(404).json({ message: 'Poker room not found.' });
|
||||
}
|
||||
});
|
||||
router.post("/create", async (req, res) => {
|
||||
const { creatorId, minBet, fakeMoney } = req.body;
|
||||
if (!creatorId) return res.status(400).json({ message: "Creator ID is required." });
|
||||
|
||||
router.post('/create', async (req, res) => {
|
||||
const { creatorId, minBet, fakeMoney } = req.body;
|
||||
if (!creatorId) return res.status(400).json({ message: 'Creator ID is required.' });
|
||||
if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) {
|
||||
return res.status(403).json({ message: "You are already in a poker room." });
|
||||
}
|
||||
|
||||
if (Object.values(pokerRooms).some(room => room.host_id === creatorId || room.players[creatorId])) {
|
||||
return res.status(403).json({ message: 'You are already in a poker room.' });
|
||||
}
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const id = uuidv4();
|
||||
const name = uniqueNamesGenerator({
|
||||
dictionaries: [adjectives, ["Poker"]],
|
||||
separator: " ",
|
||||
style: "capital",
|
||||
});
|
||||
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const id = uuidv4();
|
||||
const name = uniqueNamesGenerator({ dictionaries: [adjectives, ['Poker']], separator: ' ', style: 'capital' });
|
||||
pokerRooms[id] = {
|
||||
id,
|
||||
host_id: creatorId,
|
||||
host_name: creator.globalName || creator.username,
|
||||
name,
|
||||
created_at: Date.now(),
|
||||
last_move_at: null,
|
||||
players: {},
|
||||
queue: {},
|
||||
afk: {},
|
||||
pioche: initialShuffledCards(),
|
||||
tapis: [],
|
||||
dealer: null,
|
||||
sb: null,
|
||||
bb: null,
|
||||
highest_bet: 0,
|
||||
current_player: null,
|
||||
current_turn: null,
|
||||
playing: false,
|
||||
winners: [],
|
||||
waiting_for_restart: false,
|
||||
fakeMoney: fakeMoney,
|
||||
minBet: minBet,
|
||||
};
|
||||
|
||||
pokerRooms[id] = {
|
||||
id, host_id: creatorId, host_name: creator.globalName || creator.username,
|
||||
name, created_at: Date.now(), last_move_at: null,
|
||||
players: {}, queue: {}, afk: {}, pioche: initialShuffledCards(), tapis: [],
|
||||
dealer: null, sb: null, bb: null, highest_bet: 0, current_player: null,
|
||||
current_turn: null, playing: false, winners: [], waiting_for_restart: false, fakeMoney: fakeMoney,
|
||||
minBet: minBet,
|
||||
};
|
||||
await joinRoom(id, creatorId, io); // Auto-join the creator
|
||||
await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" });
|
||||
|
||||
await joinRoom(id, creatorId, io); // Auto-join the creator
|
||||
await emitPokerUpdate({ room: pokerRooms[id], type: 'room-created' });
|
||||
try {
|
||||
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Flopoker 🃏")
|
||||
.setDescription(`<@${creatorId}> a créé une table de poker`)
|
||||
.addFields(
|
||||
{ name: `Nom`, value: `**${name}**`, inline: true },
|
||||
{
|
||||
name: `${fakeMoney ? "Mise initiale" : "Prix d'entrée"}`,
|
||||
value: `**${formatAmount(minBet)}** 🪙`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `Fake Money`,
|
||||
value: `${fakeMoney ? "**Oui** ✅" : "**Non** ❌"}`,
|
||||
inline: true,
|
||||
},
|
||||
)
|
||||
.setColor("#5865f2")
|
||||
.setTimestamp(new Date());
|
||||
|
||||
try {
|
||||
const generalChannel = guild.channels.cache.find(
|
||||
ch => ch.name === 'général' || ch.name === 'general'
|
||||
);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Flopoker 🃏')
|
||||
.setDescription(`<@${creatorId}> a créé une table de poker`)
|
||||
.addFields(
|
||||
{ name: `Nom`, value: `**${name}**`, inline: true },
|
||||
{ name: `${fakeMoney ? 'Mise initiale' : 'Prix d\'entrée'}`, value: `**${formatAmount(minBet)}** 🪙`, inline: true },
|
||||
{ name: `Fake Money`, value: `${fakeMoney ? '**Oui** ✅' : '**Non** ❌'}`, inline: true },
|
||||
)
|
||||
.setColor('#5865f2')
|
||||
.setTimestamp(new Date());
|
||||
const row = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel(`Rejoindre la table ${name}`)
|
||||
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
|
||||
.setStyle(ButtonStyle.Link),
|
||||
);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel(`Rejoindre la table ${name}`)
|
||||
.setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
|
||||
.setStyle(ButtonStyle.Link)
|
||||
);
|
||||
await generalChannel.send({ embeds: [embed], components: [row] });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
await generalChannel.send({ embeds: [embed], components: [row] });
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
res.status(201).json({ roomId: id });
|
||||
});
|
||||
|
||||
res.status(201).json({ roomId: id });
|
||||
});
|
||||
router.post("/join", async (req, res) => {
|
||||
const { userId, roomId } = req.body;
|
||||
if (!userId || !roomId) return res.status(400).json({ message: "User ID and Room ID are required." });
|
||||
if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." });
|
||||
if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) {
|
||||
return res.status(403).json({ message: "You are already in a room or queue." });
|
||||
}
|
||||
if (!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." });
|
||||
}
|
||||
|
||||
router.post('/join', async (req, res) => {
|
||||
const { userId, roomId } = req.body;
|
||||
if (!userId || !roomId) return res.status(400).json({ message: 'User ID and Room ID are required.' });
|
||||
if (!pokerRooms[roomId]) return res.status(404).json({ message: 'Room not found.' });
|
||||
if (Object.values(pokerRooms).some(r => r.players[userId] || r.queue[userId])) {
|
||||
return res.status(403).json({ message: 'You are already in a room or queue.' });
|
||||
}
|
||||
if (!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.' });
|
||||
}
|
||||
await joinRoom(roomId, userId, io);
|
||||
res.status(200).json({ message: "Successfully joined." });
|
||||
});
|
||||
|
||||
await joinRoom(roomId, userId, io);
|
||||
res.status(200).json({ message: 'Successfully joined.' });
|
||||
});
|
||||
router.post("/accept", async (req, res) => {
|
||||
const { hostId, playerId, roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
|
||||
return res.status(403).json({ message: "Unauthorized or player not in queue." });
|
||||
}
|
||||
|
||||
router.post('/accept', async (req, res) => {
|
||||
const { hostId, playerId, roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
|
||||
return res.status(403).json({ message: 'Unauthorized or player not in queue.' });
|
||||
}
|
||||
if (!room.fakeMoney) {
|
||||
const userDB = getUser.get(playerId);
|
||||
if (userDB) {
|
||||
updateUserCoins.run({
|
||||
id: playerId,
|
||||
coins: userDB.coins - room.minBet,
|
||||
});
|
||||
insertLog.run({
|
||||
id: `${playerId}-poker-${Date.now()}`,
|
||||
user_id: playerId,
|
||||
target_user_id: null,
|
||||
action: "POKER_JOIN",
|
||||
coins_amount: -room.minBet,
|
||||
user_new_amount: userDB.coins - room.minBet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!room.fakeMoney) {
|
||||
const userDB = getUser.get(playerId);
|
||||
if (userDB) {
|
||||
updateUserCoins.run({ id: playerId, coins: userDB.coins - room.minBet });
|
||||
insertLog.run({
|
||||
id: `${playerId}-poker-${Date.now()}`,
|
||||
user_id: playerId, target_user_id: null,
|
||||
action: 'POKER_JOIN',
|
||||
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
|
||||
})
|
||||
}
|
||||
}
|
||||
room.players[playerId] = room.queue[playerId];
|
||||
delete room.queue[playerId];
|
||||
|
||||
room.players[playerId] = room.queue[playerId];
|
||||
delete room.queue[playerId];
|
||||
await emitPokerUpdate({ room: room, type: "player-accepted" });
|
||||
res.status(200).json({ message: "Player accepted." });
|
||||
});
|
||||
|
||||
await emitPokerUpdate({ room: room, type: 'player-accepted' });
|
||||
res.status(200).json({ message: 'Player accepted.' });
|
||||
});
|
||||
router.post("/leave", async (req, res) => {
|
||||
const { userId, roomId } = req.body;
|
||||
|
||||
router.post('/leave', async (req, res) => {
|
||||
const { userId, roomId } = req.body
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
if (
|
||||
pokerRooms[roomId].playing &&
|
||||
pokerRooms[roomId].current_turn !== null &&
|
||||
pokerRooms[roomId].current_turn !== 4
|
||||
) {
|
||||
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId];
|
||||
|
||||
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
|
||||
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId]
|
||||
try {
|
||||
pokerRooms[roomId].players[userId].folded = true;
|
||||
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn;
|
||||
if (pokerRooms[roomId].current_player === userId) {
|
||||
await checkRoundCompletion(pokerRooms[roomId], io);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
try {
|
||||
pokerRooms[roomId].players[userId].folded = true
|
||||
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn
|
||||
if (pokerRooms[roomId].current_player === userId) {
|
||||
await checkRoundCompletion(pokerRooms[roomId], io);
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
}
|
||||
await emitPokerUpdate({ type: "player-afk" });
|
||||
return res.status(200);
|
||||
}
|
||||
|
||||
await emitPokerUpdate({ type: 'player-afk' });
|
||||
return res.status(200)
|
||||
}
|
||||
try {
|
||||
updatePlayerCoins(
|
||||
pokerRooms[roomId].players[userId],
|
||||
pokerRooms[roomId].players[userId].bank,
|
||||
pokerRooms[roomId].fakeMoney,
|
||||
);
|
||||
delete pokerRooms[roomId].players[userId];
|
||||
|
||||
try {
|
||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
||||
delete pokerRooms[roomId].players[userId]
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId];
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId]
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
await emitPokerUpdate({ type: "player-left" });
|
||||
return res.status(200);
|
||||
});
|
||||
|
||||
await emitPokerUpdate({ type: 'player-left' });
|
||||
return res.status(200)
|
||||
});
|
||||
router.post("/kick", async (req, res) => {
|
||||
const { commandUserId, userId, roomId } = req.body;
|
||||
|
||||
router.post('/kick', async (req, res) => {
|
||||
const { commandUserId, userId, roomId } = req.body
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" });
|
||||
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: "Seul l'host peut kick" });
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
|
||||
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: 'Seul l\'host peut kick' })
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
if (
|
||||
pokerRooms[roomId].playing &&
|
||||
pokerRooms[roomId].current_turn !== null &&
|
||||
pokerRooms[roomId].current_turn !== 4
|
||||
) {
|
||||
return res.status(403).send({ message: "Playing" });
|
||||
}
|
||||
|
||||
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
|
||||
return res.status(403).send({ message: 'Playing' })
|
||||
}
|
||||
try {
|
||||
updatePlayerCoins(
|
||||
pokerRooms[roomId].players[userId],
|
||||
pokerRooms[roomId].players[userId].bank,
|
||||
pokerRooms[roomId].fakeMoney,
|
||||
);
|
||||
delete pokerRooms[roomId].players[userId];
|
||||
|
||||
try {
|
||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
||||
delete pokerRooms[roomId].players[userId]
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId];
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId]
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
await emitPokerUpdate({ type: "player-kicked" });
|
||||
return res.status(200);
|
||||
});
|
||||
|
||||
await emitPokerUpdate({ type: 'player-kicked' });
|
||||
return res.status(200)
|
||||
});
|
||||
// --- Game Action Endpoints ---
|
||||
|
||||
// --- Game Action Endpoints ---
|
||||
router.post("/start", async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room) return res.status(404).json({ message: "Room not found." });
|
||||
if (Object.keys(room.players).length < 2) return res.status(400).json({ message: "Not enough players to start." });
|
||||
|
||||
router.post('/start', async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room) return res.status(404).json({ message: 'Room not found.' });
|
||||
if (Object.keys(room.players).length < 2) return res.status(400).json({ message: 'Not enough players to start.' });
|
||||
await startNewHand(room, io);
|
||||
res.status(200).json({ message: "Game started." });
|
||||
});
|
||||
|
||||
await startNewHand(room, io);
|
||||
res.status(200).json({ message: 'Game started.' });
|
||||
});
|
||||
// NEW: Endpoint to start the next hand
|
||||
router.post("/next-hand", async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || !room.waiting_for_restart) {
|
||||
return res.status(400).json({ message: "Not ready for the next hand." });
|
||||
}
|
||||
await startNewHand(room, io);
|
||||
res.status(200).json({ message: "Next hand started." });
|
||||
});
|
||||
|
||||
// NEW: Endpoint to start the next hand
|
||||
router.post('/next-hand', async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || !room.waiting_for_restart) {
|
||||
return res.status(400).json({ message: 'Not ready for the next hand.' });
|
||||
}
|
||||
await startNewHand(room, io);
|
||||
res.status(200).json({ message: 'Next hand started.' });
|
||||
});
|
||||
router.post("/action/:action", async (req, res) => {
|
||||
const { playerId, amount, roomId } = req.body;
|
||||
const { action } = req.params;
|
||||
const room = pokerRooms[roomId];
|
||||
|
||||
router.post('/action/:action', async (req, res) => {
|
||||
const { playerId, amount, roomId } = req.body;
|
||||
const { action } = req.params;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || !room.players[playerId] || room.current_player !== playerId) {
|
||||
return res.status(403).json({ message: "It's not your turn or you are not in this game." });
|
||||
}
|
||||
|
||||
if (!room || !room.players[playerId] || room.current_player !== playerId) {
|
||||
return res.status(403).json({ message: "It's not your turn or you are not in this game." });
|
||||
}
|
||||
const player = room.players[playerId];
|
||||
|
||||
const player = room.players[playerId];
|
||||
switch (action) {
|
||||
case "fold":
|
||||
player.folded = true;
|
||||
await emitPokerToast({
|
||||
type: "player-fold",
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
});
|
||||
break;
|
||||
case "check":
|
||||
if (player.bet < room.highest_bet) return res.status(400).json({ message: "Cannot check." });
|
||||
await emitPokerToast({
|
||||
type: "player-check",
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
});
|
||||
break;
|
||||
case "call":
|
||||
const callAmount = Math.min(room.highest_bet - player.bet, player.bank);
|
||||
player.bank -= callAmount;
|
||||
player.bet += callAmount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
await emitPokerToast({
|
||||
type: "player-call",
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
});
|
||||
break;
|
||||
case "raise":
|
||||
if (amount <= 0 || amount > player.bank || player.bet + amount <= room.highest_bet) {
|
||||
return res.status(400).json({ message: "Invalid raise amount." });
|
||||
}
|
||||
player.bank -= amount;
|
||||
player.bet += amount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
room.highest_bet = player.bet;
|
||||
await emitPokerToast({
|
||||
type: "player-raise",
|
||||
amount: amount,
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({ message: "Invalid action." });
|
||||
}
|
||||
|
||||
switch(action) {
|
||||
case 'fold':
|
||||
player.folded = true;
|
||||
await emitPokerToast({
|
||||
type: 'player-fold',
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
})
|
||||
break;
|
||||
case 'check':
|
||||
if (player.bet < room.highest_bet) return res.status(400).json({ message: 'Cannot check.' });
|
||||
await emitPokerToast({
|
||||
type: 'player-check',
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
})
|
||||
break;
|
||||
case 'call':
|
||||
const callAmount = Math.min(room.highest_bet - player.bet, player.bank);
|
||||
player.bank -= callAmount;
|
||||
player.bet += callAmount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
await emitPokerToast({
|
||||
type: 'player-call',
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
})
|
||||
break;
|
||||
case 'raise':
|
||||
if (amount <= 0 || amount > player.bank || (player.bet + amount) <= room.highest_bet) {
|
||||
return res.status(400).json({ message: 'Invalid raise amount.' });
|
||||
}
|
||||
player.bank -= amount;
|
||||
player.bet += amount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
room.highest_bet = player.bet;
|
||||
await emitPokerToast({
|
||||
type: 'player-raise',
|
||||
amount: amount,
|
||||
playerId: player.id,
|
||||
playerName: player.globalName,
|
||||
roomId: room.id,
|
||||
})
|
||||
break;
|
||||
default: return res.status(400).json({ message: 'Invalid action.' });
|
||||
}
|
||||
player.last_played_turn = room.current_turn;
|
||||
await checkRoundCompletion(room, io);
|
||||
res.status(200).json({ message: `Action '${action}' successful.` });
|
||||
});
|
||||
|
||||
player.last_played_turn = room.current_turn;
|
||||
await checkRoundCompletion(room, io);
|
||||
res.status(200).json({ message: `Action '${action}' successful.` });
|
||||
});
|
||||
|
||||
return router;
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
async function joinRoom(roomId, userId, io) {
|
||||
const user = await client.users.fetch(userId);
|
||||
const userDB = getUser.get(userId);
|
||||
const room = pokerRooms[roomId];
|
||||
const user = await client.users.fetch(userId);
|
||||
const userDB = getUser.get(userId);
|
||||
const room = pokerRooms[roomId];
|
||||
|
||||
const playerObject = {
|
||||
id: userId, globalName: user.globalName || user.username, avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
hand: [], bank: room.minBet, bet: 0, folded: false, allin: false,
|
||||
last_played_turn: null, solve: null
|
||||
};
|
||||
const playerObject = {
|
||||
id: userId,
|
||||
globalName: user.globalName || user.username,
|
||||
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
hand: [],
|
||||
bank: room.minBet,
|
||||
bet: 0,
|
||||
folded: false,
|
||||
allin: false,
|
||||
last_played_turn: null,
|
||||
solve: null,
|
||||
};
|
||||
|
||||
if (room.playing) {
|
||||
room.queue[userId] = playerObject;
|
||||
} else {
|
||||
room.players[userId] = playerObject;
|
||||
if (!room.fakeMoney) {
|
||||
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
|
||||
insertLog.run({
|
||||
id: `${userId}-poker-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'POKER_JOIN',
|
||||
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (room.playing) {
|
||||
room.queue[userId] = playerObject;
|
||||
} else {
|
||||
room.players[userId] = playerObject;
|
||||
if (!room.fakeMoney) {
|
||||
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
|
||||
insertLog.run({
|
||||
id: `${userId}-poker-${Date.now()}`,
|
||||
user_id: userId,
|
||||
target_user_id: null,
|
||||
action: "POKER_JOIN",
|
||||
coins_amount: -room.minBet,
|
||||
user_new_amount: userDB.coins - room.minBet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await emitPokerUpdate({ room: room, type: 'player-joined' });
|
||||
await emitPokerUpdate({ room: room, type: "player-joined" });
|
||||
}
|
||||
|
||||
async function startNewHand(room, io) {
|
||||
const playerIds = Object.keys(room.players);
|
||||
if (playerIds.length < 2) {
|
||||
room.playing = false; // Not enough players to continue
|
||||
await emitPokerUpdate({ room: room, type: 'new-hand' });
|
||||
return;
|
||||
}
|
||||
const playerIds = Object.keys(room.players);
|
||||
if (playerIds.length < 2) {
|
||||
room.playing = false; // Not enough players to continue
|
||||
await emitPokerUpdate({ room: room, type: "new-hand" });
|
||||
return;
|
||||
}
|
||||
|
||||
room.playing = true;
|
||||
room.current_turn = 0; // Pre-flop
|
||||
room.pioche = initialShuffledCards();
|
||||
room.tapis = [];
|
||||
room.winners = [];
|
||||
room.waiting_for_restart = false;
|
||||
room.highest_bet = 20;
|
||||
room.last_move_at = Date.now();
|
||||
room.playing = true;
|
||||
room.current_turn = 0; // Pre-flop
|
||||
room.pioche = initialShuffledCards();
|
||||
room.tapis = [];
|
||||
room.winners = [];
|
||||
room.waiting_for_restart = false;
|
||||
room.highest_bet = 20;
|
||||
room.last_move_at = Date.now();
|
||||
|
||||
// Rotate dealer
|
||||
const oldDealerIndex = playerIds.indexOf(room.dealer);
|
||||
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
|
||||
// Rotate dealer
|
||||
const oldDealerIndex = playerIds.indexOf(room.dealer);
|
||||
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
|
||||
|
||||
Object.values(room.players).forEach(p => {
|
||||
p.hand = [room.pioche.pop(), room.pioche.pop()];
|
||||
p.bet = 0; p.folded = false; p.allin = false; p.last_played_turn = null;
|
||||
});
|
||||
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
|
||||
Object.values(room.players).forEach((p) => {
|
||||
p.hand = [room.pioche.pop(), room.pioche.pop()];
|
||||
p.bet = 0;
|
||||
p.folded = false;
|
||||
p.allin = false;
|
||||
p.last_played_turn = null;
|
||||
});
|
||||
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
|
||||
|
||||
// Handle blinds based on new dealer
|
||||
const dealerIndex = playerIds.indexOf(room.dealer);
|
||||
const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]];
|
||||
const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]];
|
||||
room.sb = sbPlayer.id;
|
||||
room.bb = bbPlayer.id;
|
||||
// Handle blinds based on new dealer
|
||||
const dealerIndex = playerIds.indexOf(room.dealer);
|
||||
const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]];
|
||||
const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]];
|
||||
room.sb = sbPlayer.id;
|
||||
room.bb = bbPlayer.id;
|
||||
|
||||
sbPlayer.bank -= 10; sbPlayer.bet = 10;
|
||||
bbPlayer.bank -= 20; bbPlayer.bet = 20;
|
||||
sbPlayer.bank -= 10;
|
||||
sbPlayer.bet = 10;
|
||||
bbPlayer.bank -= 20;
|
||||
bbPlayer.bet = 20;
|
||||
|
||||
bbPlayer.last_played_turn = 0;
|
||||
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
|
||||
await emitPokerUpdate({ room: room, type: 'room-started' });
|
||||
bbPlayer.last_played_turn = 0;
|
||||
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
|
||||
await emitPokerUpdate({ room: room, type: "room-started" });
|
||||
}
|
||||
|
||||
async function checkRoundCompletion(room, io) {
|
||||
room.last_move_at = Date.now();
|
||||
const roundResult = checkEndOfBettingRound(room);
|
||||
room.last_move_at = Date.now();
|
||||
const roundResult = checkEndOfBettingRound(room);
|
||||
|
||||
if (roundResult.endRound) {
|
||||
if (roundResult.winner) {
|
||||
await handleShowdown(room, io, [roundResult.winner]);
|
||||
} else {
|
||||
await advanceToNextPhase(room, io, roundResult.nextPhase);
|
||||
}
|
||||
} else {
|
||||
room.current_player = getNextActivePlayer(room);
|
||||
await emitPokerUpdate({ room: room, type: 'round-continue' });
|
||||
}
|
||||
if (roundResult.endRound) {
|
||||
if (roundResult.winner) {
|
||||
await handleShowdown(room, io, [roundResult.winner]);
|
||||
} else {
|
||||
await advanceToNextPhase(room, io, roundResult.nextPhase);
|
||||
}
|
||||
} else {
|
||||
room.current_player = getNextActivePlayer(room);
|
||||
await emitPokerUpdate({ room: room, type: "round-continue" });
|
||||
}
|
||||
}
|
||||
|
||||
async function advanceToNextPhase(room, io, phase) {
|
||||
Object.values(room.players).forEach(p => { if (!p.folded) p.last_played_turn = null; });
|
||||
Object.values(room.players).forEach((p) => {
|
||||
if (!p.folded) p.last_played_turn = null;
|
||||
});
|
||||
|
||||
switch(phase) {
|
||||
case 'flop':
|
||||
room.current_turn = 1;
|
||||
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
|
||||
break;
|
||||
case 'turn':
|
||||
room.current_turn = 2;
|
||||
room.tapis.push(room.pioche.pop());
|
||||
break;
|
||||
case 'river':
|
||||
room.current_turn = 3;
|
||||
room.tapis.push(room.pioche.pop());
|
||||
break;
|
||||
case 'showdown':
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
case 'progressive-showdown':
|
||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
||||
while(room.tapis.length < 5) {
|
||||
await sleep(500);
|
||||
room.tapis.push(room.pioche.pop());
|
||||
updatePlayerHandSolves(room);
|
||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
||||
}
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
}
|
||||
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
|
||||
room.current_player = getFirstActivePlayerAfterDealer(room);
|
||||
await emitPokerUpdate({ room: room, type: 'phase-advanced' });
|
||||
switch (phase) {
|
||||
case "flop":
|
||||
room.current_turn = 1;
|
||||
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
|
||||
break;
|
||||
case "turn":
|
||||
room.current_turn = 2;
|
||||
room.tapis.push(room.pioche.pop());
|
||||
break;
|
||||
case "river":
|
||||
room.current_turn = 3;
|
||||
room.tapis.push(room.pioche.pop());
|
||||
break;
|
||||
case "showdown":
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
case "progressive-showdown":
|
||||
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
|
||||
while (room.tapis.length < 5) {
|
||||
await sleep(500);
|
||||
room.tapis.push(room.pioche.pop());
|
||||
updatePlayerHandSolves(room);
|
||||
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
|
||||
}
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
}
|
||||
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
|
||||
room.current_player = getFirstActivePlayerAfterDealer(room);
|
||||
await emitPokerUpdate({ room: room, type: "phase-advanced" });
|
||||
}
|
||||
|
||||
async function handleShowdown(room, io, winners) {
|
||||
room.current_turn = 4;
|
||||
room.playing = false;
|
||||
room.waiting_for_restart = true;
|
||||
room.winners = winners;
|
||||
room.current_player = null;
|
||||
room.current_turn = 4;
|
||||
room.playing = false;
|
||||
room.waiting_for_restart = true;
|
||||
room.winners = winners;
|
||||
room.current_player = null;
|
||||
|
||||
let totalPot = 0;
|
||||
Object.values(room.players).forEach(p => { totalPot += p.bet; });
|
||||
let totalPot = 0;
|
||||
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 => {
|
||||
const winnerPlayer = room.players[winnerId];
|
||||
if(winnerPlayer) {
|
||||
winnerPlayer.bank += winAmount;
|
||||
}
|
||||
});
|
||||
winners.forEach((winnerId) => {
|
||||
const winnerPlayer = room.players[winnerId];
|
||||
if (winnerPlayer) {
|
||||
winnerPlayer.bank += winAmount;
|
||||
}
|
||||
});
|
||||
|
||||
await clearAfkPlayers(room);
|
||||
await clearAfkPlayers(room);
|
||||
|
||||
//await pokerEloHandler(room);
|
||||
await emitPokerUpdate({ room: room, type: 'showdown' });
|
||||
await emitPokerToast({
|
||||
type: 'player-winner',
|
||||
playerIds: winners,
|
||||
roomId: room.id,
|
||||
amount: winAmount,
|
||||
})
|
||||
//await pokerEloHandler(room);
|
||||
await emitPokerUpdate({ room: room, type: "showdown" });
|
||||
await emitPokerToast({
|
||||
type: "player-winner",
|
||||
playerIds: winners,
|
||||
roomId: room.id,
|
||||
amount: winAmount,
|
||||
});
|
||||
}
|
||||
|
||||
// NEW: Function to calculate and update hand strength for all players
|
||||
function updatePlayerHandSolves(room) {
|
||||
const communityCards = room.tapis;
|
||||
for (const player of Object.values(room.players)) {
|
||||
if (!player.folded) {
|
||||
const allCards = [...communityCards, ...player.hand];
|
||||
player.solve = Hand.solve(allCards).descr;
|
||||
}
|
||||
}
|
||||
const communityCards = room.tapis;
|
||||
for (const player of Object.values(room.players)) {
|
||||
if (!player.folded) {
|
||||
const allCards = [...communityCards, ...player.hand];
|
||||
player.solve = Hand.solve(allCards).descr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlayerCoins(player, amount, isFake) {
|
||||
if (isFake) return;
|
||||
const user = getUser.get(player.id);
|
||||
if (!user) return;
|
||||
if (isFake) return;
|
||||
const user = getUser.get(player.id);
|
||||
if (!user) return;
|
||||
|
||||
const userDB = getUser.get(player.id);
|
||||
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
|
||||
insertLog.run({
|
||||
id: `${player.id}-poker-${Date.now()}`,
|
||||
user_id: player.id, target_user_id: null,
|
||||
action: `POKER_${amount > 0 ? 'WIN' : 'LOSE'}`,
|
||||
coins_amount: amount, user_new_amount: userDB.coins + amount,
|
||||
});
|
||||
const userDB = getUser.get(player.id);
|
||||
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
|
||||
insertLog.run({
|
||||
id: `${player.id}-poker-${Date.now()}`,
|
||||
user_id: player.id,
|
||||
target_user_id: null,
|
||||
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
|
||||
coins_amount: amount,
|
||||
user_new_amount: userDB.coins + amount,
|
||||
});
|
||||
}
|
||||
|
||||
async function clearAfkPlayers(room) {
|
||||
Object.keys(room.afk).forEach(playerId => {
|
||||
if (room.players[playerId]) {
|
||||
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
|
||||
delete room.players[playerId];
|
||||
}
|
||||
});
|
||||
room.afk = {};
|
||||
Object.keys(room.afk).forEach((playerId) => {
|
||||
if (room.players[playerId]) {
|
||||
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
|
||||
delete room.players[playerId];
|
||||
}
|
||||
});
|
||||
room.afk = {};
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
|
||||
// --- Game Logic Imports ---
|
||||
import {
|
||||
createDeck, shuffle, deal, isValidMove, moveCard, drawCard,
|
||||
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves
|
||||
} from '../../game/solitaire.js';
|
||||
createDeck,
|
||||
shuffle,
|
||||
deal,
|
||||
isValidMove,
|
||||
moveCard,
|
||||
drawCard,
|
||||
checkWinCondition,
|
||||
createSeededRNG,
|
||||
seededShuffle,
|
||||
undoMove,
|
||||
draw3Cards,
|
||||
checkAutoSolve,
|
||||
autoSolveMoves,
|
||||
} from "../../game/solitaire.js";
|
||||
|
||||
// --- Game State & Database Imports ---
|
||||
import { activeSolitaireGames } from '../../game/state.js';
|
||||
import { activeSolitaireGames } from "../../game/state.js";
|
||||
import {
|
||||
getSOTD, getUser, insertSOTDStats, deleteUserSOTDStats,
|
||||
getUserSOTDStats, updateUserCoins, insertLog, getAllSOTDStats
|
||||
} from '../../database/index.js';
|
||||
import {socketEmit} from "../socket.js";
|
||||
getSOTD,
|
||||
getUser,
|
||||
insertSOTDStats,
|
||||
deleteUserSOTDStats,
|
||||
getUserSOTDStats,
|
||||
updateUserCoins,
|
||||
insertLog,
|
||||
getAllSOTDStats,
|
||||
} from "../../database/index.js";
|
||||
import { socketEmit } from "../socket.js";
|
||||
|
||||
// Create a new router instance
|
||||
const router = express.Router();
|
||||
@@ -24,248 +41,262 @@ const router = express.Router();
|
||||
* @returns {object} The configured Express router.
|
||||
*/
|
||||
export function solitaireRoutes(client, io) {
|
||||
// --- Game Initialization Endpoints ---
|
||||
|
||||
// --- Game Initialization Endpoints ---
|
||||
router.post("/start", (req, res) => {
|
||||
const { userId, userSeed, hardMode } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: "User ID is required." });
|
||||
|
||||
router.post('/start', (req, res) => {
|
||||
const { userId, userSeed, hardMode } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: 'User ID is required.' });
|
||||
// If a game already exists for the user, return it instead of creating a new one.
|
||||
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
|
||||
return res.json({
|
||||
success: true,
|
||||
gameState: activeSolitaireGames[userId],
|
||||
});
|
||||
}
|
||||
|
||||
// If a game already exists for the user, return it instead of creating a new one.
|
||||
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
|
||||
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
|
||||
}
|
||||
let deck, seed;
|
||||
if (userSeed) {
|
||||
// Use the provided seed to create a deterministic game
|
||||
seed = userSeed;
|
||||
} else {
|
||||
// Create a random seed if none is provided
|
||||
seed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
let deck, seed;
|
||||
if (userSeed) {
|
||||
// Use the provided seed to create a deterministic game
|
||||
seed = userSeed;
|
||||
} else {
|
||||
// Create a random seed if none is provided
|
||||
seed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
let numericSeed = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xffffffff;
|
||||
}
|
||||
|
||||
let numericSeed = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xFFFFFFFF;
|
||||
}
|
||||
const rng = createSeededRNG(numericSeed);
|
||||
deck = seededShuffle(createDeck(), rng);
|
||||
|
||||
const rng = createSeededRNG(numericSeed);
|
||||
deck = seededShuffle(createDeck(), rng);
|
||||
const gameState = deal(deck);
|
||||
gameState.seed = seed;
|
||||
gameState.isSOTD = false;
|
||||
gameState.score = 0;
|
||||
gameState.moves = 0;
|
||||
gameState.hist = [];
|
||||
gameState.hardMode = hardMode ?? false;
|
||||
gameState.autocompleting = false;
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
|
||||
const gameState = deal(deck);
|
||||
gameState.seed = seed;
|
||||
gameState.isSOTD = false;
|
||||
gameState.score = 0;
|
||||
gameState.moves = 0;
|
||||
gameState.hist = [];
|
||||
gameState.hardMode = hardMode ?? false;
|
||||
gameState.autocompleting = false;
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
router.post('/start/sotd', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
/*if (!userId || !getUser.get(userId)) {
|
||||
router.post("/start/sotd", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
/*if (!userId || !getUser.get(userId)) {
|
||||
return res.status(404).json({ error: 'User not found.' });
|
||||
}*/
|
||||
|
||||
if (activeSolitaireGames[userId]?.isSOTD) {
|
||||
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
|
||||
}
|
||||
if (activeSolitaireGames[userId]?.isSOTD) {
|
||||
return res.json({
|
||||
success: true,
|
||||
gameState: activeSolitaireGames[userId],
|
||||
});
|
||||
}
|
||||
|
||||
const sotd = getSOTD.get();
|
||||
if (!sotd) {
|
||||
return res.status(500).json({ error: 'Solitaire of the Day is not configured.'});
|
||||
}
|
||||
const sotd = getSOTD.get();
|
||||
if (!sotd) {
|
||||
return res.status(500).json({ error: "Solitaire of the Day is not configured." });
|
||||
}
|
||||
|
||||
const gameState = {
|
||||
tableauPiles: JSON.parse(sotd.tableauPiles),
|
||||
foundationPiles: JSON.parse(sotd.foundationPiles),
|
||||
stockPile: JSON.parse(sotd.stockPile),
|
||||
wastePile: JSON.parse(sotd.wastePile),
|
||||
isDone: false,
|
||||
isSOTD: true,
|
||||
startTime: Date.now(),
|
||||
endTime: null,
|
||||
moves: 0,
|
||||
score: 0,
|
||||
seed: sotd.seed,
|
||||
hist: [],
|
||||
hardMode: false,
|
||||
autocompleting: false,
|
||||
};
|
||||
const gameState = {
|
||||
tableauPiles: JSON.parse(sotd.tableauPiles),
|
||||
foundationPiles: JSON.parse(sotd.foundationPiles),
|
||||
stockPile: JSON.parse(sotd.stockPile),
|
||||
wastePile: JSON.parse(sotd.wastePile),
|
||||
isDone: false,
|
||||
isSOTD: true,
|
||||
startTime: Date.now(),
|
||||
endTime: null,
|
||||
moves: 0,
|
||||
score: 0,
|
||||
seed: sotd.seed,
|
||||
hist: [],
|
||||
hardMode: false,
|
||||
autocompleting: false,
|
||||
};
|
||||
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
// --- Game State & Action Endpoints ---
|
||||
// --- Game State & Action Endpoints ---
|
||||
|
||||
router.get('/sotd/rankings', (req, res) => {
|
||||
try {
|
||||
const rankings = getAllSOTDStats.all();
|
||||
res.json({ rankings });
|
||||
} catch(e) {
|
||||
res.status(500).json({ error: "Failed to fetch SOTD rankings."});
|
||||
}
|
||||
});
|
||||
router.get("/sotd/rankings", (req, res) => {
|
||||
try {
|
||||
const rankings = getAllSOTDStats.all();
|
||||
res.json({ rankings });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to fetch SOTD rankings." });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/state/:userId', (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
if (gameState) {
|
||||
res.json({ success: true, gameState });
|
||||
} else {
|
||||
res.status(404).json({ error: 'No active game found for this user.' });
|
||||
}
|
||||
});
|
||||
router.get("/state/:userId", (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
if (gameState) {
|
||||
res.json({ success: true, gameState });
|
||||
} else {
|
||||
res.status(404).json({ error: "No active game found for this user." });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/reset', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (activeSolitaireGames[userId]) {
|
||||
delete activeSolitaireGames[userId];
|
||||
}
|
||||
res.json({ success: true, message: "Game reset."});
|
||||
});
|
||||
router.post("/reset", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (activeSolitaireGames[userId]) {
|
||||
delete activeSolitaireGames[userId];
|
||||
}
|
||||
res.json({ success: true, message: "Game reset." });
|
||||
});
|
||||
|
||||
router.post('/move', async (req, res) => {
|
||||
const { userId, ...moveData } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
router.post("/move", async (req, res) => {
|
||||
const { userId, ...moveData } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||
|
||||
if (isValidMove(gameState, moveData)) {
|
||||
moveCard(gameState, moveData);
|
||||
updateGameStats(gameState, 'move', moveData);
|
||||
if (isValidMove(gameState, moveData)) {
|
||||
moveCard(gameState, moveData);
|
||||
updateGameStats(gameState, "move", moveData);
|
||||
|
||||
if (!gameState.autocompleting) {
|
||||
const canAutoSolve = checkAutoSolve(gameState);
|
||||
if (canAutoSolve) {
|
||||
gameState.autocompleting = true;
|
||||
autoSolveMoves(userId, gameState)
|
||||
}
|
||||
}
|
||||
if (!gameState.autocompleting) {
|
||||
const canAutoSolve = checkAutoSolve(gameState);
|
||||
if (canAutoSolve) {
|
||||
gameState.autocompleting = true;
|
||||
autoSolveMoves(userId, gameState);
|
||||
}
|
||||
}
|
||||
|
||||
const win = checkWinCondition(gameState);
|
||||
if (win) {
|
||||
gameState.isDone = true;
|
||||
await handleWin(userId, gameState, io);
|
||||
}
|
||||
res.json({ success: true, gameState, win });
|
||||
} else {
|
||||
res.status(400).json({ error: 'Invalid move' });
|
||||
}
|
||||
});
|
||||
const win = checkWinCondition(gameState);
|
||||
if (win) {
|
||||
gameState.isDone = true;
|
||||
await handleWin(userId, gameState, io);
|
||||
}
|
||||
res.json({ success: true, gameState, win });
|
||||
} else {
|
||||
res.status(400).json({ error: "Invalid move" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/draw', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
router.post("/draw", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||
|
||||
if (gameState.hardMode) {
|
||||
draw3Cards(gameState);
|
||||
} else {
|
||||
drawCard(gameState);
|
||||
}
|
||||
updateGameStats(gameState, 'draw');
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
if (gameState.hardMode) {
|
||||
draw3Cards(gameState);
|
||||
} else {
|
||||
drawCard(gameState);
|
||||
}
|
||||
updateGameStats(gameState, "draw");
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
router.post('/undo', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
router.post("/undo", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
||||
if (gameState.hist.length === 0) return res.status(400).json({ error: 'No moves to undo.'});
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||
if (gameState.hist.length === 0) return res.status(400).json({ error: "No moves to undo." });
|
||||
|
||||
undoMove(gameState);
|
||||
res.json({ success: true, gameState });
|
||||
})
|
||||
undoMove(gameState);
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
return router;
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
/** Updates game stats like moves and score after an action. */
|
||||
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++;
|
||||
if (actionType === 'move') {
|
||||
if (moveData.destPileType === 'foundationPiles') {
|
||||
gameState.score += 10; // Move card to foundation
|
||||
}
|
||||
if (moveData.sourcePileType === 'foundationPiles') {
|
||||
gameState.score -= 15; // Move card from foundation (penalty)
|
||||
}
|
||||
}
|
||||
if(actionType === 'draw' && gameState.wastePile.length === 0) {
|
||||
// Penalty for cycling through an empty stock pile
|
||||
gameState.score -= 5;
|
||||
}
|
||||
gameState.moves++;
|
||||
if (actionType === "move") {
|
||||
if (moveData.destPileType === "foundationPiles") {
|
||||
gameState.score += 10; // Move card to foundation
|
||||
}
|
||||
if (moveData.sourcePileType === "foundationPiles") {
|
||||
gameState.score -= 15; // Move card from foundation (penalty)
|
||||
}
|
||||
}
|
||||
if (actionType === "draw" && gameState.wastePile.length === 0) {
|
||||
// Penalty for cycling through an empty stock pile
|
||||
gameState.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles the logic when a game is won. */
|
||||
async function handleWin(userId, gameState, io) {
|
||||
const currentUser = getUser.get(userId);
|
||||
if (!currentUser) return;
|
||||
const currentUser = getUser.get(userId);
|
||||
if (!currentUser) return;
|
||||
|
||||
if (gameState.hardMode) {
|
||||
const bonus = 100;
|
||||
const newCoins = currentUser.coins + bonus;
|
||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||
insertLog.run({
|
||||
id: `${userId}-hardmode-solitaire-${Date.now()}`, user_id: userId,
|
||||
action: 'HARDMODE_SOLITAIRE_WIN', target_user_id: null,
|
||||
coins_amount: bonus, user_new_amount: newCoins,
|
||||
});
|
||||
await socketEmit('data-updated', { table: 'users' });
|
||||
}
|
||||
if (gameState.hardMode) {
|
||||
const bonus = 100;
|
||||
const newCoins = currentUser.coins + bonus;
|
||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||
insertLog.run({
|
||||
id: `${userId}-hardmode-solitaire-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: "HARDMODE_SOLITAIRE_WIN",
|
||||
target_user_id: null,
|
||||
coins_amount: bonus,
|
||||
user_new_amount: newCoins,
|
||||
});
|
||||
await socketEmit("data-updated", { table: "users" });
|
||||
}
|
||||
|
||||
if (!gameState.isSOTD) return; // Only process SOTD wins here
|
||||
if (!gameState.isSOTD) return; // Only process SOTD wins here
|
||||
|
||||
gameState.endTime = Date.now();
|
||||
const timeTaken = gameState.endTime - gameState.startTime;
|
||||
gameState.endTime = Date.now();
|
||||
const timeTaken = gameState.endTime - gameState.startTime;
|
||||
|
||||
const existingStats = getUserSOTDStats.get(userId);
|
||||
const existingStats = getUserSOTDStats.get(userId);
|
||||
|
||||
if (!existingStats) {
|
||||
// First time completing the SOTD, grant bonus coins
|
||||
const bonus = 1000;
|
||||
const newCoins = currentUser.coins + bonus;
|
||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||
insertLog.run({
|
||||
id: `${userId}-sotd-complete-${Date.now()}`, user_id: userId,
|
||||
action: 'SOTD_WIN', target_user_id: null,
|
||||
coins_amount: bonus, user_new_amount: newCoins,
|
||||
});
|
||||
await socketEmit('data-updated', { table: 'users' });
|
||||
}
|
||||
if (!existingStats) {
|
||||
// First time completing the SOTD, grant bonus coins
|
||||
const bonus = 1000;
|
||||
const newCoins = currentUser.coins + bonus;
|
||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||
insertLog.run({
|
||||
id: `${userId}-sotd-complete-${Date.now()}`,
|
||||
user_id: userId,
|
||||
action: "SOTD_WIN",
|
||||
target_user_id: null,
|
||||
coins_amount: bonus,
|
||||
user_new_amount: newCoins,
|
||||
});
|
||||
await socketEmit("data-updated", { table: "users" });
|
||||
}
|
||||
|
||||
// Save the score if it's better than the previous one
|
||||
const isNewBest = !existingStats ||
|
||||
gameState.score > existingStats.score ||
|
||||
(gameState.score === existingStats.score && gameState.moves < existingStats.moves) ||
|
||||
(gameState.score === existingStats.score && gameState.moves === existingStats.moves && timeTaken < existingStats.time);
|
||||
// Save the score if it's better than the previous one
|
||||
const isNewBest =
|
||||
!existingStats ||
|
||||
gameState.score > existingStats.score ||
|
||||
(gameState.score === existingStats.score && gameState.moves < existingStats.moves) ||
|
||||
(gameState.score === existingStats.score &&
|
||||
gameState.moves === existingStats.moves &&
|
||||
timeTaken < existingStats.time);
|
||||
|
||||
if (isNewBest) {
|
||||
deleteUserSOTDStats.run(userId)
|
||||
insertSOTDStats.run({
|
||||
id: userId, user_id: userId,
|
||||
time: timeTaken,
|
||||
moves: gameState.moves,
|
||||
score: gameState.score,
|
||||
});
|
||||
await socketEmit('sotd-update')
|
||||
console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`);
|
||||
}
|
||||
if (isNewBest) {
|
||||
deleteUserSOTDStats.run(userId);
|
||||
insertSOTDStats.run({
|
||||
id: userId,
|
||||
user_id: userId,
|
||||
time: timeTaken,
|
||||
moves: gameState.moves,
|
||||
score: gameState.score,
|
||||
});
|
||||
await socketEmit("sotd-update");
|
||||
console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||
import {
|
||||
activeTicTacToeGames,
|
||||
tictactoeQueue,
|
||||
activeConnect4Games,
|
||||
connect4Queue,
|
||||
queueMessagesEndpoints, activePredis
|
||||
} from '../game/state.js';
|
||||
import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js';
|
||||
import { eloHandler } from '../game/elo.js';
|
||||
activeTicTacToeGames,
|
||||
tictactoeQueue,
|
||||
activeConnect4Games,
|
||||
connect4Queue,
|
||||
queueMessagesEndpoints,
|
||||
activePredis,
|
||||
} from "../game/state.js";
|
||||
import {
|
||||
createConnect4Board,
|
||||
formatConnect4BoardForDiscord,
|
||||
checkConnect4Win,
|
||||
checkConnect4Draw,
|
||||
C4_ROWS,
|
||||
} from "../game/various.js";
|
||||
import { eloHandler } from "../game/elo.js";
|
||||
import { getUser } from "../database/index.js";
|
||||
|
||||
// --- Module-level State ---
|
||||
@@ -16,70 +23,73 @@ let io;
|
||||
// --- Main Initialization Function ---
|
||||
|
||||
export function initializeSocket(server, client) {
|
||||
io = server;
|
||||
io = server;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('user-connected', async (userId) => {
|
||||
if (!userId) return;
|
||||
await refreshQueuesForUser(userId, client);
|
||||
});
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("user-connected", async (userId) => {
|
||||
if (!userId) return;
|
||||
await refreshQueuesForUser(userId, client);
|
||||
});
|
||||
|
||||
registerTicTacToeEvents(socket, client);
|
||||
registerConnect4Events(socket, client);
|
||||
registerTicTacToeEvents(socket, client);
|
||||
registerConnect4Events(socket, client);
|
||||
|
||||
socket.on('tictactoe:queue:leave', async ({ discordId }) => await refreshQueuesForUser(discordId, client));
|
||||
socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
|
||||
|
||||
// catch tab kills / network drops
|
||||
socket.on('disconnecting', async () => {
|
||||
const discordId = socket.handshake.auth?.discordId; // or your mapping
|
||||
await refreshQueuesForUser(discordId, client);
|
||||
});
|
||||
// catch tab kills / network drops
|
||||
socket.on("disconnecting", async () => {
|
||||
const discordId = socket.handshake.auth?.discordId; // or your mapping
|
||||
await refreshQueuesForUser(discordId, client);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(cleanupStaleGames, 5 * 60 * 1000);
|
||||
setInterval(cleanupStaleGames, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
export function getSocketIo() {
|
||||
return io;
|
||||
return io;
|
||||
}
|
||||
|
||||
// --- Event Registration ---
|
||||
|
||||
function registerTicTacToeEvents(socket, client) {
|
||||
socket.on('tictactoeconnection', (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on('tictactoequeue', (e) => onQueueJoin(client, 'tictactoe', e.playerId));
|
||||
socket.on('tictactoeplaying', (e) => onTicTacToeMove(client, e));
|
||||
socket.on('tictactoegameOver', (e) => onGameOver(client, 'tictactoe', e.playerId, e.winner));
|
||||
socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId));
|
||||
socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e));
|
||||
socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner));
|
||||
}
|
||||
|
||||
function registerConnect4Events(socket, client) {
|
||||
socket.on('connect4connection', (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on('connect4queue', (e) => onQueueJoin(client, 'connect4', e.playerId));
|
||||
socket.on('connect4playing', (e) => onConnect4Move(client, e));
|
||||
socket.on('connect4NoTime', (e) => onGameOver(client, 'connect4', e.playerId, e.winner, '(temps écoulé)'));
|
||||
socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId));
|
||||
socket.on("connect4playing", (e) => onConnect4Move(client, e));
|
||||
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
|
||||
}
|
||||
|
||||
// --- Core Handlers (Preserving Original Logic) ---
|
||||
|
||||
async function onQueueJoin(client, gameType, playerId) {
|
||||
if (!playerId) return;
|
||||
const { queue, activeGames, title, url } = getGameAssets(gameType);
|
||||
if (!playerId) return;
|
||||
const { queue, activeGames, title, url } = getGameAssets(gameType);
|
||||
|
||||
if (queue.includes(playerId) || Object.values(activeGames).some(g => g.p1.id === playerId || g.p2.id === playerId)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
queue.includes(playerId) ||
|
||||
Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
queue.push(playerId);
|
||||
console.log(`[${title}] Player ${playerId} joined the queue.`);
|
||||
queue.push(playerId);
|
||||
console.log(`[${title}] Player ${playerId} joined the queue.`);
|
||||
|
||||
if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url);
|
||||
if (queue.length >= 2) await createGame(client, gameType);
|
||||
if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url);
|
||||
if (queue.length >= 2) await createGame(client, gameType);
|
||||
|
||||
await emitQueueUpdate(client, gameType);
|
||||
await emitQueueUpdate(client, gameType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,262 +98,365 @@ async function onQueueJoin(client, gameType, playerId) {
|
||||
* @returns {boolean} - True if the player has won, false otherwise.
|
||||
*/
|
||||
function checkTicTacToeWin(moves) {
|
||||
const winningCombinations = [
|
||||
[1, 2, 3], [4, 5, 6], [7, 8, 9], // Rows
|
||||
[1, 4, 7], [2, 5, 8], [3, 6, 9], // Columns
|
||||
[1, 5, 9], [3, 5, 7] // Diagonals
|
||||
];
|
||||
for (const combination of winningCombinations) {
|
||||
if (combination.every(num => moves.includes(num))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const winningCombinations = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9], // Rows
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[3, 6, 9], // Columns
|
||||
[1, 5, 9],
|
||||
[3, 5, 7], // Diagonals
|
||||
];
|
||||
for (const combination of winningCombinations) {
|
||||
if (combination.every((num) => moves.includes(num))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function onTicTacToeMove(client, eventData) {
|
||||
const { playerId, value, boxId } = eventData;
|
||||
const lobby = Object.values(activeTicTacToeGames).find(g => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver);
|
||||
if (!lobby) return;
|
||||
const { playerId, value, boxId } = eventData;
|
||||
const lobby = Object.values(activeTicTacToeGames).find(
|
||||
(g) => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver,
|
||||
);
|
||||
if (!lobby) return;
|
||||
|
||||
const isP1Turn = lobby.sum % 2 === 1 && value === 'X' && lobby.p1.id === playerId;
|
||||
const isP2Turn = lobby.sum % 2 === 0 && value === 'O' && lobby.p2.id === playerId;
|
||||
const isP1Turn = lobby.sum % 2 === 1 && value === "X" && lobby.p1.id === playerId;
|
||||
const isP2Turn = lobby.sum % 2 === 0 && value === "O" && lobby.p2.id === playerId;
|
||||
|
||||
if (isP1Turn || isP2Turn) {
|
||||
const playerMoves = isP1Turn ? lobby.xs : lobby.os;
|
||||
playerMoves.push(boxId);
|
||||
lobby.sum++;
|
||||
lobby.lastmove = Date.now();
|
||||
if (isP1Turn || isP2Turn) {
|
||||
const playerMoves = isP1Turn ? lobby.xs : lobby.os;
|
||||
playerMoves.push(boxId);
|
||||
lobby.sum++;
|
||||
lobby.lastmove = Date.now();
|
||||
|
||||
if (isP1Turn) lobby.p1.move = boxId
|
||||
if (isP2Turn) lobby.p2.move = boxId
|
||||
if (isP1Turn) lobby.p1.move = boxId;
|
||||
if (isP2Turn) lobby.p2.move = boxId;
|
||||
|
||||
io.emit('tictactoeplaying', { allPlayers: Object.values(activeTicTacToeGames) });
|
||||
const hasWon = checkTicTacToeWin(playerMoves);
|
||||
if (hasWon) {
|
||||
// The current player has won. End the game.
|
||||
await onGameOver(client, 'tictactoe', playerId, playerId);
|
||||
} else if (lobby.sum > 9) {
|
||||
// It's a draw (9 moves made, sum is now 10). End the game.
|
||||
await onGameOver(client, 'tictactoe', playerId, null); // null winner for a draw
|
||||
} else {
|
||||
// The game continues. Update the state and notify clients.
|
||||
await updateDiscordMessage(client, lobby, 'Tic Tac Toe');
|
||||
}
|
||||
}
|
||||
await emitQueueUpdate(client, 'tictactoe');
|
||||
io.emit("tictactoeplaying", {
|
||||
allPlayers: Object.values(activeTicTacToeGames),
|
||||
});
|
||||
const hasWon = checkTicTacToeWin(playerMoves);
|
||||
if (hasWon) {
|
||||
// The current player has won. End the game.
|
||||
await onGameOver(client, "tictactoe", playerId, playerId);
|
||||
} else if (lobby.sum > 9) {
|
||||
// It's a draw (9 moves made, sum is now 10). End the game.
|
||||
await onGameOver(client, "tictactoe", playerId, null); // null winner for a draw
|
||||
} else {
|
||||
// The game continues. Update the state and notify clients.
|
||||
await updateDiscordMessage(client, lobby, "Tic Tac Toe");
|
||||
}
|
||||
}
|
||||
await emitQueueUpdate(client, "tictactoe");
|
||||
}
|
||||
|
||||
async function onConnect4Move(client, eventData) {
|
||||
const { playerId, col } = eventData;
|
||||
const lobby = Object.values(activeConnect4Games).find(l => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver);
|
||||
if (!lobby || lobby.turn !== playerId) return;
|
||||
const { playerId, col } = eventData;
|
||||
const lobby = Object.values(activeConnect4Games).find(
|
||||
(l) => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver,
|
||||
);
|
||||
if (!lobby || lobby.turn !== playerId) return;
|
||||
|
||||
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
|
||||
let row;
|
||||
for (row = C4_ROWS - 1; row >= 0; row--) {
|
||||
if (lobby.board[row][col] === null) {
|
||||
lobby.board[row][col] = player.val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row < 0) return;
|
||||
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
|
||||
let row;
|
||||
for (row = C4_ROWS - 1; row >= 0; row--) {
|
||||
if (lobby.board[row][col] === null) {
|
||||
lobby.board[row][col] = player.val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row < 0) return;
|
||||
|
||||
lobby.lastmove = Date.now();
|
||||
const winCheck = checkConnect4Win(lobby.board, player.val);
|
||||
lobby.lastmove = Date.now();
|
||||
const winCheck = checkConnect4Win(lobby.board, player.val);
|
||||
|
||||
let winnerId = null;
|
||||
if (winCheck.win) {
|
||||
lobby.winningPieces = winCheck.pieces;
|
||||
winnerId = player.id;
|
||||
} else if (checkConnect4Draw(lobby.board)) {
|
||||
winnerId = null; // Represents a draw
|
||||
} else {
|
||||
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
|
||||
io.emit('connect4playing', { allPlayers: Object.values(activeConnect4Games) });
|
||||
await emitQueueUpdate(client, 'connact4');
|
||||
await updateDiscordMessage(client, lobby, 'Puissance 4');
|
||||
return;
|
||||
}
|
||||
await onGameOver(client, 'connect4', playerId, winnerId);
|
||||
let winnerId = null;
|
||||
if (winCheck.win) {
|
||||
lobby.winningPieces = winCheck.pieces;
|
||||
winnerId = player.id;
|
||||
} else if (checkConnect4Draw(lobby.board)) {
|
||||
winnerId = null; // Represents a draw
|
||||
} else {
|
||||
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
|
||||
io.emit("connect4playing", {
|
||||
allPlayers: Object.values(activeConnect4Games),
|
||||
});
|
||||
await emitQueueUpdate(client, "connact4");
|
||||
await updateDiscordMessage(client, lobby, "Puissance 4");
|
||||
return;
|
||||
}
|
||||
await onGameOver(client, "connect4", playerId, winnerId);
|
||||
}
|
||||
|
||||
async function onGameOver(client, gameType, playerId, winnerId, reason = '') {
|
||||
const { activeGames, title } = getGameAssets(gameType);
|
||||
const gameKey = Object.keys(activeGames).find(key => key.includes(playerId));
|
||||
const game = gameKey ? activeGames[gameKey] : undefined;
|
||||
if (!game || game.gameOver) return;
|
||||
async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
|
||||
const { activeGames, title } = getGameAssets(gameType);
|
||||
const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId));
|
||||
const game = gameKey ? activeGames[gameKey] : undefined;
|
||||
if (!game || game.gameOver) return;
|
||||
|
||||
game.gameOver = true;
|
||||
let resultText;
|
||||
if (winnerId === null) {
|
||||
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
|
||||
resultText = 'Égalité';
|
||||
} else {
|
||||
await eloHandler(game.p1.id, game.p2.id, game.p1.id === winnerId ? 1 : 0, game.p2.id === winnerId ? 1 : 0, title.toUpperCase());
|
||||
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
|
||||
resultText = `Victoire de ${winnerName}`;
|
||||
}
|
||||
game.gameOver = true;
|
||||
let resultText;
|
||||
if (winnerId === null) {
|
||||
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
|
||||
resultText = "Égalité";
|
||||
} else {
|
||||
await eloHandler(
|
||||
game.p1.id,
|
||||
game.p2.id,
|
||||
game.p1.id === winnerId ? 1 : 0,
|
||||
game.p2.id === winnerId ? 1 : 0,
|
||||
title.toUpperCase(),
|
||||
);
|
||||
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
|
||||
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 === 'connect4') io.emit('connect4gameOver', { game, winner: winnerId });
|
||||
if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
|
||||
if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
|
||||
|
||||
if (gameKey) {
|
||||
setTimeout(() => delete activeGames[gameKey], 1000)
|
||||
}
|
||||
if (gameKey) {
|
||||
setTimeout(() => delete activeGames[gameKey], 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Game Lifecycle & Discord Helpers ---
|
||||
|
||||
async function createGame(client, gameType) {
|
||||
const { queue, activeGames, title } = getGameAssets(gameType);
|
||||
const p1Id = queue.shift();
|
||||
const p2Id = queue.shift();
|
||||
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
|
||||
const { queue, activeGames, title } = getGameAssets(gameType);
|
||||
const p1Id = queue.shift();
|
||||
const p2Id = queue.shift();
|
||||
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
|
||||
|
||||
let lobby;
|
||||
if (gameType === 'tictactoe') {
|
||||
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'X', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'O', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, sum: 1, xs: [], os: [], gameOver: false, lastmove: Date.now() };
|
||||
} else { // connect4
|
||||
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'R', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'Y', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, turn: p1Id, board: createConnect4Board(), gameOver: false, lastmove: Date.now(), winningPieces: [] };
|
||||
}
|
||||
let lobby;
|
||||
if (gameType === "tictactoe") {
|
||||
lobby = {
|
||||
p1: {
|
||||
id: p1Id,
|
||||
name: p1.globalName,
|
||||
val: "X",
|
||||
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
},
|
||||
p2: {
|
||||
id: p2Id,
|
||||
name: p2.globalName,
|
||||
val: "O",
|
||||
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
},
|
||||
sum: 1,
|
||||
xs: [],
|
||||
os: [],
|
||||
gameOver: false,
|
||||
lastmove: Date.now(),
|
||||
};
|
||||
} else {
|
||||
// connect4
|
||||
lobby = {
|
||||
p1: {
|
||||
id: p1Id,
|
||||
name: p1.globalName,
|
||||
val: "R",
|
||||
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
},
|
||||
p2: {
|
||||
id: p2Id,
|
||||
name: p2.globalName,
|
||||
val: "Y",
|
||||
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
},
|
||||
turn: p1Id,
|
||||
board: createConnect4Board(),
|
||||
gameOver: false,
|
||||
lastmove: Date.now(),
|
||||
winningPieces: [],
|
||||
};
|
||||
}
|
||||
|
||||
const msgId = await updateDiscordMessage(client, lobby, title);
|
||||
lobby.msgId = msgId;
|
||||
const msgId = await updateDiscordMessage(client, lobby, title);
|
||||
lobby.msgId = msgId;
|
||||
|
||||
const gameKey = `${p1Id}-${p2Id}`;
|
||||
activeGames[gameKey] = lobby;
|
||||
const gameKey = `${p1Id}-${p2Id}`;
|
||||
activeGames[gameKey] = lobby;
|
||||
|
||||
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
|
||||
await emitQueueUpdate(client, gameType);
|
||||
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
|
||||
await emitQueueUpdate(client, gameType);
|
||||
}
|
||||
|
||||
// --- Utility Functions ---
|
||||
|
||||
async function refreshQueuesForUser(userId, client) {
|
||||
// FIX: Mutate the array instead of reassigning it.
|
||||
let index = tictactoeQueue.indexOf(userId);
|
||||
if (index > -1) {
|
||||
tictactoeQueue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
|
||||
const updatedEmbed = new EmbedBuilder().setTitle('Tic Tac Toe').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
|
||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
delete queueMessagesEndpoints[userId];
|
||||
} catch (e) {
|
||||
console.error('Error updating queue message : ', e);
|
||||
}
|
||||
}
|
||||
// FIX: Mutate the array instead of reassigning it.
|
||||
let index = tictactoeQueue.indexOf(userId);
|
||||
if (index > -1) {
|
||||
tictactoeQueue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setTitle("Tic Tac Toe")
|
||||
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
|
||||
.setColor(0xed4245)
|
||||
.setTimestamp(new Date());
|
||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
delete queueMessagesEndpoints[userId];
|
||||
} catch (e) {
|
||||
console.error("Error updating queue message : ", e);
|
||||
}
|
||||
}
|
||||
|
||||
index = connect4Queue.indexOf(userId);
|
||||
if (index > -1) {
|
||||
connect4Queue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
|
||||
const updatedEmbed = new EmbedBuilder().setTitle('Puissance 4').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
|
||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
delete queueMessagesEndpoints[userId];
|
||||
} catch (e) {
|
||||
console.error('Error updating queue message : ', e);
|
||||
}
|
||||
}
|
||||
index = connect4Queue.indexOf(userId);
|
||||
if (index > -1) {
|
||||
connect4Queue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setTitle("Puissance 4")
|
||||
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
|
||||
.setColor(0xed4245)
|
||||
.setTimestamp(new Date());
|
||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
delete queueMessagesEndpoints[userId];
|
||||
} catch (e) {
|
||||
console.error("Error updating queue message : ", e);
|
||||
}
|
||||
}
|
||||
|
||||
await emitQueueUpdate(client, 'tictactoe');
|
||||
await emitQueueUpdate(client, 'connect4');
|
||||
await emitQueueUpdate(client, "tictactoe");
|
||||
await emitQueueUpdate(client, "connect4");
|
||||
}
|
||||
|
||||
async function emitQueueUpdate(client, gameType) {
|
||||
const { queue, activeGames } = getGameAssets(gameType);
|
||||
const names = await Promise.all(queue.map(async (id) => {
|
||||
const user = await client.users.fetch(id).catch(() => null);
|
||||
return user?.globalName || user?.username;
|
||||
}));
|
||||
io.emit(`${gameType}queue`, { allPlayers: Object.values(activeGames), queue: names.filter(Boolean) });
|
||||
const { queue, activeGames } = getGameAssets(gameType);
|
||||
const names = await Promise.all(
|
||||
queue.map(async (id) => {
|
||||
const user = await client.users.fetch(id).catch(() => null);
|
||||
return user?.globalName || user?.username;
|
||||
}),
|
||||
);
|
||||
io.emit(`${gameType}queue`, {
|
||||
allPlayers: Object.values(activeGames),
|
||||
queue: names.filter(Boolean),
|
||||
});
|
||||
}
|
||||
|
||||
function getGameAssets(gameType) {
|
||||
if (gameType === 'tictactoe') return { queue: tictactoeQueue, activeGames: activeTicTacToeGames, title: 'Tic Tac Toe', url: '/tic-tac-toe' };
|
||||
if (gameType === 'connect4') return { queue: connect4Queue, activeGames: activeConnect4Games, title: 'Puissance 4', url: '/connect-4' };
|
||||
return { queue: [], activeGames: {} };
|
||||
if (gameType === "tictactoe")
|
||||
return {
|
||||
queue: tictactoeQueue,
|
||||
activeGames: activeTicTacToeGames,
|
||||
title: "Tic Tac Toe",
|
||||
url: "/tic-tac-toe",
|
||||
};
|
||||
if (gameType === "connect4")
|
||||
return {
|
||||
queue: connect4Queue,
|
||||
activeGames: activeConnect4Games,
|
||||
title: "Puissance 4",
|
||||
url: "/connect-4",
|
||||
};
|
||||
return { queue: [], activeGames: {} };
|
||||
}
|
||||
|
||||
async function postQueueToDiscord(client, playerId, title, url) {
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(playerId);
|
||||
const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2').setTimestamp(new Date());
|
||||
const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel(`Jouer contre ${user.username}`).setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`).setStyle(ButtonStyle.Link));
|
||||
const msg = await generalChannel.send({ embeds: [embed], components: [row] });
|
||||
queueMessagesEndpoints[playerId] = msg.id
|
||||
} catch (e) { console.error(`Failed to post queue message for ${title}:`, e); }
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||
const user = await client.users.fetch(playerId);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`)
|
||||
.setColor("#5865F2")
|
||||
.setTimestamp(new Date());
|
||||
const row = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel(`Jouer contre ${user.username}`)
|
||||
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`)
|
||||
.setStyle(ButtonStyle.Link),
|
||||
);
|
||||
const msg = await generalChannel.send({
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
});
|
||||
queueMessagesEndpoints[playerId] = msg.id;
|
||||
} catch (e) {
|
||||
console.error(`Failed to post queue message for ${title}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDiscordMessage(client, game, title, resultText = '') {
|
||||
const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
|
||||
if (!channel) return null;
|
||||
async function updateDiscordMessage(client, game, title, resultText = "") {
|
||||
const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
|
||||
if (!channel) return null;
|
||||
|
||||
let description;
|
||||
if (title === 'Tic Tac Toe') {
|
||||
let gridText = '';
|
||||
for (let i = 1; i <= 9; i++) { gridText += game.xs.includes(i) ? '❌' : game.os.includes(i) ? '⭕' : '🟦'; if (i % 3 === 0) gridText += '\n'; }
|
||||
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
|
||||
} else {
|
||||
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
|
||||
}
|
||||
if (resultText) description += `\n### ${resultText}`;
|
||||
let description;
|
||||
if (title === "Tic Tac Toe") {
|
||||
let gridText = "";
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
gridText += game.xs.includes(i) ? "❌" : game.os.includes(i) ? "⭕" : "🟦";
|
||||
if (i % 3 === 0) gridText += "\n";
|
||||
}
|
||||
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
|
||||
} else {
|
||||
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
|
||||
}
|
||||
if (resultText) description += `\n### ${resultText}`;
|
||||
|
||||
const embed = new EmbedBuilder().setTitle(title).setDescription(description).setColor(game.gameOver ? '#2ade2a' : '#5865f2');
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setDescription(description)
|
||||
.setColor(game.gameOver ? "#2ade2a" : "#5865f2");
|
||||
|
||||
try {
|
||||
if (game.msgId) {
|
||||
const message = await channel.messages.fetch(game.msgId);
|
||||
await message.edit({ embeds: [embed] });
|
||||
return game.msgId;
|
||||
} else {
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
return message.id;
|
||||
}
|
||||
} catch (e) { return null; }
|
||||
try {
|
||||
if (game.msgId) {
|
||||
const message = await channel.messages.fetch(game.msgId);
|
||||
await message.edit({ embeds: [embed] });
|
||||
return game.msgId;
|
||||
} else {
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
return message.id;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupStaleGames() {
|
||||
const now = Date.now();
|
||||
const STALE_TIMEOUT = 30 * 60 * 1000;
|
||||
const cleanup = (games, name) => {
|
||||
Object.keys(games).forEach(key => {
|
||||
if (now - games[key].lastmove > STALE_TIMEOUT) {
|
||||
console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
|
||||
delete games[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
cleanup(activeTicTacToeGames, 'TicTacToe');
|
||||
cleanup(activeConnect4Games, 'Connect4');
|
||||
const now = Date.now();
|
||||
const STALE_TIMEOUT = 30 * 60 * 1000;
|
||||
const cleanup = (games, name) => {
|
||||
Object.keys(games).forEach((key) => {
|
||||
if (now - games[key].lastmove > STALE_TIMEOUT) {
|
||||
console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
|
||||
delete games[key];
|
||||
}
|
||||
});
|
||||
};
|
||||
cleanup(activeTicTacToeGames, "TicTacToe");
|
||||
cleanup(activeConnect4Games, "Connect4");
|
||||
}
|
||||
|
||||
/* EMITS */
|
||||
export async function socketEmit(event, data) {
|
||||
io.emit(event, data);
|
||||
io.emit(event, data);
|
||||
}
|
||||
export async function emitDataUpdated(data) {
|
||||
io.emit('data-updated', data);
|
||||
io.emit("data-updated", data);
|
||||
}
|
||||
|
||||
export async function emitPokerUpdate(data) {
|
||||
io.emit('poker-update', data);
|
||||
io.emit("poker-update", data);
|
||||
}
|
||||
|
||||
export async function emitPokerToast(data) {
|
||||
io.emit('poker-toast', data);
|
||||
io.emit("poker-toast", data);
|
||||
}
|
||||
|
||||
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
|
||||
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
||||
export const 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 });
|
||||
|
||||
268
src/utils/ai.js
268
src/utils/ai.js
@@ -1,27 +1,26 @@
|
||||
import 'dotenv/config';
|
||||
import "dotenv/config";
|
||||
import OpenAI from "openai";
|
||||
import {GoogleGenAI} from "@google/genai";
|
||||
import {Mistral} from '@mistralai/mistralai';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { Mistral } from "@mistralai/mistralai";
|
||||
|
||||
// --- AI Client Initialization ---
|
||||
// Initialize clients for each AI service. This is done once when the module is loaded.
|
||||
|
||||
let openai;
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
openai = new OpenAI();
|
||||
openai = new OpenAI();
|
||||
}
|
||||
|
||||
let gemini;
|
||||
if (process.env.GEMINI_KEY) {
|
||||
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY})
|
||||
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY });
|
||||
}
|
||||
|
||||
let mistral;
|
||||
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.
|
||||
* It dynamically chooses the provider based on the MODEL environment variable.
|
||||
@@ -29,175 +28,180 @@ if (process.env.MISTRAL_KEY) {
|
||||
* @returns {Promise<string>} The content of the AI's response message.
|
||||
*/
|
||||
export async function gork(messageHistory) {
|
||||
const modelProvider = process.env.MODEL;
|
||||
const modelProvider = process.env.MODEL;
|
||||
|
||||
console.log(`[AI] Requesting completion from ${modelProvider}...`);
|
||||
console.log(`[AI] Requesting completion from ${modelProvider}...`);
|
||||
|
||||
try {
|
||||
// --- OpenAI Provider ---
|
||||
if (modelProvider === 'OpenAI' && openai) {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-5", // Using a modern, cost-effective model
|
||||
reasoning_effort: "low",
|
||||
messages: messageHistory,
|
||||
});
|
||||
return completion.choices[0].message.content;
|
||||
}
|
||||
try {
|
||||
// --- OpenAI Provider ---
|
||||
if (modelProvider === "OpenAI" && openai) {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-5", // Using a modern, cost-effective model
|
||||
reasoning_effort: "low",
|
||||
messages: messageHistory,
|
||||
});
|
||||
return completion.choices[0].message.content;
|
||||
}
|
||||
|
||||
// --- Google Gemini Provider ---
|
||||
else if (modelProvider === 'Gemini' && gemini) {
|
||||
// Gemini requires a slightly different history format.
|
||||
const contents = messageHistory.map(msg => ({
|
||||
role: msg.role === 'assistant' ? 'model' : msg.role, // Gemini uses 'model' for assistant role
|
||||
parts: [{ text: msg.content }],
|
||||
}));
|
||||
// --- Google Gemini Provider ---
|
||||
else if (modelProvider === "Gemini" && gemini) {
|
||||
// Gemini requires a slightly different history format.
|
||||
const contents = messageHistory.map((msg) => ({
|
||||
role: msg.role === "assistant" ? "model" : msg.role, // Gemini uses 'model' for assistant role
|
||||
parts: [{ text: msg.content }],
|
||||
}));
|
||||
|
||||
// The last message should not be from the model
|
||||
if (contents[contents.length - 1].role === 'model') {
|
||||
contents.pop();
|
||||
}
|
||||
// The last message should not be from the model
|
||||
if (contents[contents.length - 1].role === "model") {
|
||||
contents.pop();
|
||||
}
|
||||
|
||||
const result = await gemini.generateContent({ contents });
|
||||
const response = await result.response;
|
||||
return response.text();
|
||||
}
|
||||
const result = await gemini.generateContent({ contents });
|
||||
const response = await result.response;
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// --- Mistral Provider ---
|
||||
else if (modelProvider === 'Mistral' && mistral) {
|
||||
const chatResponse = await mistral.chat({
|
||||
model: 'mistral-large-latest',
|
||||
messages: messageHistory,
|
||||
});
|
||||
return chatResponse.choices[0].message.content;
|
||||
}
|
||||
// --- Mistral Provider ---
|
||||
else if (modelProvider === "Mistral" && mistral) {
|
||||
const chatResponse = await mistral.chat({
|
||||
model: "mistral-large-latest",
|
||||
messages: messageHistory,
|
||||
});
|
||||
return chatResponse.choices[0].message.content;
|
||||
}
|
||||
|
||||
// --- Fallback Case ---
|
||||
else {
|
||||
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.";
|
||||
}
|
||||
} catch(error) {
|
||||
console.error(`[AI] Error with ${modelProvider} API:`, error);
|
||||
return "Oups, une erreur est survenue en contactant le service d'IA.";
|
||||
}
|
||||
// --- Fallback Case ---
|
||||
else {
|
||||
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.";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[AI] Error with ${modelProvider} API:`, error);
|
||||
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 MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || '3', 10);
|
||||
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || 'true') === 'true';
|
||||
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || "100", 10);
|
||||
export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || "3", 10);
|
||||
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || "true") === "true";
|
||||
|
||||
export const stripMentionsOfBot = (text, botId) =>
|
||||
text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim();
|
||||
export const stripMentionsOfBot = (text, botId) => text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
|
||||
|
||||
export const sanitize = (s) =>
|
||||
(s || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels
|
||||
.trim();
|
||||
(s || "")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/```/g, "ʼʼʼ") // éviter de casser des fences éventuels
|
||||
.trim();
|
||||
|
||||
export const shortTs = (d) => new Date(d).toISOString(); // compact et triable
|
||||
|
||||
export function buildParticipantsMap(messages) {
|
||||
const map = {};
|
||||
for (const m of messages) {
|
||||
const id = m.author.id;
|
||||
if (!map[id]) {
|
||||
map[id] = {
|
||||
id,
|
||||
username: m.author.username,
|
||||
globalName: m.author.globalName || null,
|
||||
isBot: !!m.author.bot,
|
||||
};
|
||||
}
|
||||
}
|
||||
return map;
|
||||
const map = {};
|
||||
for (const m of messages) {
|
||||
const id = m.author.id;
|
||||
if (!map[id]) {
|
||||
map[id] = {
|
||||
id,
|
||||
username: m.author.username,
|
||||
globalName: m.author.globalName || null,
|
||||
isBot: !!m.author.bot,
|
||||
};
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function buildTranscript(messages, botId) {
|
||||
// Oldest -> newest, JSONL compact, une ligne par message pertinent
|
||||
const lines = [];
|
||||
for (const m of messages) {
|
||||
const content = sanitize(m.content);
|
||||
const atts = Array.from(m.attachments?.values?.() || []);
|
||||
if (!content && atts.length === 0) continue;
|
||||
// Oldest -> newest, JSONL compact, une ligne par message pertinent
|
||||
const lines = [];
|
||||
for (const m of messages) {
|
||||
const content = sanitize(m.content);
|
||||
const atts = Array.from(m.attachments?.values?.() || []);
|
||||
if (!content && atts.length === 0) continue;
|
||||
|
||||
const attMeta = atts.length
|
||||
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
type: a.contentType || 'application/octet-stream',
|
||||
size: a.size,
|
||||
isImage: !!(a.contentType && a.contentType.startsWith('image/')),
|
||||
width: a.width || undefined,
|
||||
height: a.height || undefined,
|
||||
spoiler: typeof a.spoiler === 'boolean' ? a.spoiler : false,
|
||||
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
|
||||
}))
|
||||
: undefined;
|
||||
const attMeta = atts.length
|
||||
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map((a) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
type: a.contentType || "application/octet-stream",
|
||||
size: a.size,
|
||||
isImage: !!(a.contentType && a.contentType.startsWith("image/")),
|
||||
width: a.width || undefined,
|
||||
height: a.height || undefined,
|
||||
spoiler: typeof a.spoiler === "boolean" ? a.spoiler : false,
|
||||
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
const line = {
|
||||
t: shortTs(m.createdTimestamp || Date.now()),
|
||||
id: m.author.id,
|
||||
nick: m.member?.nickname || m.author.globalName || m.author.username,
|
||||
isBot: !!m.author.bot,
|
||||
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''),
|
||||
replyTo: m.reference?.messageId || null,
|
||||
content,
|
||||
attachments: attMeta,
|
||||
};
|
||||
lines.push(line);
|
||||
}
|
||||
return lines.map(l => JSON.stringify(l)).join('\n');
|
||||
const line = {
|
||||
t: shortTs(m.createdTimestamp || Date.now()),
|
||||
id: m.author.id,
|
||||
nick: m.member?.nickname || m.author.globalName || m.author.username,
|
||||
isBot: !!m.author.bot,
|
||||
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ""),
|
||||
replyTo: m.reference?.messageId || null,
|
||||
content,
|
||||
attachments: attMeta,
|
||||
};
|
||||
lines.push(line);
|
||||
}
|
||||
return lines.map((l) => JSON.stringify(l)).join("\n");
|
||||
}
|
||||
|
||||
export function buildAiMessages({
|
||||
botId,
|
||||
botName = 'FlopoBot',
|
||||
invokerId,
|
||||
invokerName,
|
||||
requestText,
|
||||
transcript,
|
||||
participants,
|
||||
repliedUserId,
|
||||
invokerAttachments = [],
|
||||
botId,
|
||||
botName = "FlopoBot",
|
||||
invokerId,
|
||||
invokerName,
|
||||
requestText,
|
||||
transcript,
|
||||
participants,
|
||||
repliedUserId,
|
||||
invokerAttachments = [],
|
||||
}) {
|
||||
const system = {
|
||||
role: 'system',
|
||||
content:
|
||||
`Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
|
||||
const system = {
|
||||
role: "system",
|
||||
content: `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
|
||||
Règles de sortie:
|
||||
- Réponds en français, en 1–3 phrases.
|
||||
- Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif.
|
||||
- Pas de "Untel a dit…", pas de longs préambules.
|
||||
- Utilise <@ID> pour mentionner quelqu'un.
|
||||
- Tu ne peux PAS ouvrir les liens; si des pièces jointes existent, tu peux simplement les mentionner (ex: "ta photo", "le PDF").`,
|
||||
};
|
||||
};
|
||||
|
||||
const attLines = invokerAttachments.length
|
||||
? invokerAttachments.map(a => `- ${a.name} (${a.type || 'type inconnu'}, ${a.size ?? '?'} o${a.isImage ? ', image' : ''})`).join('\n')
|
||||
: '';
|
||||
const attLines = invokerAttachments.length
|
||||
? invokerAttachments
|
||||
.map((a) => `- ${a.name} (${a.type || "type inconnu"}, ${a.size ?? "?"} o${a.isImage ? ", image" : ""})`)
|
||||
.join("\n")
|
||||
: "";
|
||||
|
||||
const user = {
|
||||
role: 'user',
|
||||
content:
|
||||
`Tâche: répondre brièvement à <@${invokerId}>.
|
||||
const user = {
|
||||
role: "user",
|
||||
content: `Tâche: répondre brièvement à <@${invokerId}>.
|
||||
|
||||
Message de <@${invokerId}> (${invokerName || 'inconnu'}):
|
||||
Message de <@${invokerId}> (${invokerName || "inconnu"}):
|
||||
"""
|
||||
${requestText}
|
||||
"""
|
||||
${invokerAttachments.length ? `Pièces jointes du message:
|
||||
${
|
||||
invokerAttachments.length
|
||||
? `Pièces jointes du message:
|
||||
${attLines}
|
||||
` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''}
|
||||
`
|
||||
: ""
|
||||
}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ""}
|
||||
|
||||
Participants (id -> nom):
|
||||
${Object.values(participants).map(p => `- ${p.id} -> ${p.globalName || p.username}`).join('\n')}
|
||||
${Object.values(participants)
|
||||
.map((p) => `- ${p.id} -> ${p.globalName || p.username}`)
|
||||
.join("\n")}
|
||||
|
||||
Contexte (transcript JSONL; à utiliser seulement si utile):
|
||||
\`\`\`jsonl
|
||||
${transcript}
|
||||
\`\`\``,
|
||||
};
|
||||
};
|
||||
|
||||
return [system, user];
|
||||
return [system, user];
|
||||
}
|
||||
@@ -1,75 +1,75 @@
|
||||
export const roles = {
|
||||
erynie_1: {
|
||||
name: 'Erinye',
|
||||
subtitle: 'Mégère, la haine',
|
||||
descr: '',
|
||||
powers: {
|
||||
double_vote: {
|
||||
descr: 'Les Erinyes peuvent tuer une deuxième personne (1 seule fois).',
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
passive: {},
|
||||
team: 'Erinyes',
|
||||
},
|
||||
erynie_2: {
|
||||
name: 'Erinye',
|
||||
subtitle: 'Tisiphone, la vengeance',
|
||||
descr: '',
|
||||
powers: {
|
||||
one_shot: {
|
||||
descr: 'Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).',
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
passive: {},
|
||||
team: 'Erinyes',
|
||||
},
|
||||
erynie_3: {
|
||||
name: 'Erinye',
|
||||
subtitle: 'Alecto, l\'implacable',
|
||||
descr: '',
|
||||
powers: {
|
||||
silence: {
|
||||
descr: 'Empêche l\'utilisation du pouvoir de quelqu\'un pour le prochain tour.',
|
||||
charges: 999,
|
||||
disabled: false,
|
||||
}
|
||||
},
|
||||
passive: {
|
||||
descr: 'Voit quels pouvoirs ont été utilisés.',
|
||||
disabled: false,
|
||||
},
|
||||
team: 'Erinyes',
|
||||
},
|
||||
narcisse: {
|
||||
name: 'Narcisse',
|
||||
subtitle: '',
|
||||
descr: '',
|
||||
powers: {},
|
||||
passive: {
|
||||
descr: 'S\'il devient maire ...',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
charon: {
|
||||
name: 'Charon',
|
||||
subtitle: 'Sorcier',
|
||||
descr: 'C\'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.',
|
||||
powers: {
|
||||
revive: {
|
||||
descr: 'Refuser de faire traverser le Styx (sauver quelqu\'un)',
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
kill: {
|
||||
descr: 'Traverser le Styx (tuer quelqu\'un)',
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
//...
|
||||
}
|
||||
erynie_1: {
|
||||
name: "Erinye",
|
||||
subtitle: "Mégère, la haine",
|
||||
descr: "",
|
||||
powers: {
|
||||
double_vote: {
|
||||
descr: "Les Erinyes peuvent tuer une deuxième personne (1 seule fois).",
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
passive: {},
|
||||
team: "Erinyes",
|
||||
},
|
||||
erynie_2: {
|
||||
name: "Erinye",
|
||||
subtitle: "Tisiphone, la vengeance",
|
||||
descr: "",
|
||||
powers: {
|
||||
one_shot: {
|
||||
descr: "Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).",
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
passive: {},
|
||||
team: "Erinyes",
|
||||
},
|
||||
erynie_3: {
|
||||
name: "Erinye",
|
||||
subtitle: "Alecto, l'implacable",
|
||||
descr: "",
|
||||
powers: {
|
||||
silence: {
|
||||
descr: "Empêche l'utilisation du pouvoir de quelqu'un pour le prochain tour.",
|
||||
charges: 999,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
passive: {
|
||||
descr: "Voit quels pouvoirs ont été utilisés.",
|
||||
disabled: false,
|
||||
},
|
||||
team: "Erinyes",
|
||||
},
|
||||
narcisse: {
|
||||
name: "Narcisse",
|
||||
subtitle: "",
|
||||
descr: "",
|
||||
powers: {},
|
||||
passive: {
|
||||
descr: "S'il devient maire ...",
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
charon: {
|
||||
name: "Charon",
|
||||
subtitle: "Sorcier",
|
||||
descr: "C'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.",
|
||||
powers: {
|
||||
revive: {
|
||||
descr: "Refuser de faire traverser le Styx (sauver quelqu'un)",
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
kill: {
|
||||
descr: "Traverser le Styx (tuer quelqu'un)",
|
||||
charges: 1,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
//...
|
||||
};
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import 'dotenv/config';
|
||||
import cron from 'node-cron';
|
||||
import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
|
||||
import "dotenv/config";
|
||||
import cron from "node-cron";
|
||||
|
||||
// --- Local Imports ---
|
||||
import { getValorantSkins, getSkinTiers } from '../api/valorant.js';
|
||||
import { DiscordRequest } from '../api/discord.js';
|
||||
import { initTodaysSOTD } from '../game/points.js';
|
||||
import { getSkinTiers, getValorantSkins } from "../api/valorant.js";
|
||||
import { DiscordRequest } from "../api/discord.js";
|
||||
import { initTodaysSOTD } from "../game/points.js";
|
||||
import {
|
||||
insertManyUsers, insertManySkins, resetDailyReward,
|
||||
pruneOldLogs, getAllUsers as dbGetAllUsers, getSOTD, getUser, getAllUsers, insertUser, stmtUsers,
|
||||
} from '../database/index.js';
|
||||
import { activeInventories, activeSearchs, activePredis, pokerRooms, skins } from '../game/state.js';
|
||||
getAllAkhys,
|
||||
getAllUsers,
|
||||
insertManySkins,
|
||||
insertUser,
|
||||
resetDailyReward,
|
||||
updateUserAvatar,
|
||||
} from "../database/index.js";
|
||||
import { activeInventories, activeSearchs, skins } from "../game/state.js";
|
||||
|
||||
export async function InstallGlobalCommands(appId, commands) {
|
||||
// API endpoint to overwrite global commands
|
||||
const endpoint = `applications/${appId}/commands`;
|
||||
// API endpoint to overwrite global commands
|
||||
const endpoint = `applications/${appId}/commands`;
|
||||
|
||||
try {
|
||||
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
|
||||
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
try {
|
||||
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
|
||||
await DiscordRequest(endpoint, { method: "PUT", body: commands });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Fetching & Initialization ---
|
||||
@@ -32,74 +35,74 @@ export async function InstallGlobalCommands(appId, commands) {
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
*/
|
||||
export async function getAkhys(client) {
|
||||
try {
|
||||
// 1. Fetch Discord Members
|
||||
const initial_akhys = getAllUsers.all().length;
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const members = await guild.members.fetch();
|
||||
const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
|
||||
try {
|
||||
// 1. Fetch Discord Members
|
||||
const initial_akhys = getAllUsers.all().length;
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const members = await guild.members.fetch();
|
||||
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
|
||||
|
||||
const usersToInsert = akhys.map((akhy) => ({
|
||||
id: akhy.user.id,
|
||||
username: akhy.user.username,
|
||||
globalName: akhy.user.globalName,
|
||||
warned: 0,
|
||||
warns: 0,
|
||||
allTimeWarns: 0,
|
||||
totalRequests: 0,
|
||||
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
isAkhy: 1,
|
||||
}));
|
||||
|
||||
const usersToInsert = akhys.map(akhy => ({
|
||||
id: akhy.user.id,
|
||||
username: akhy.user.username,
|
||||
globalName: akhy.user.globalName,
|
||||
warned: 0,
|
||||
warns: 0,
|
||||
allTimeWarns: 0,
|
||||
totalRequests: 0,
|
||||
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
isAkhy: 1
|
||||
}));
|
||||
if (usersToInsert.length > 0) {
|
||||
usersToInsert.forEach((user) => {
|
||||
try {
|
||||
insertUser.run(user);
|
||||
} catch (err) {}
|
||||
});
|
||||
}
|
||||
|
||||
if (usersToInsert.length > 0) {
|
||||
usersToInsert.forEach(user => {
|
||||
try { insertUser.run(user) } catch (err) {}
|
||||
})
|
||||
}
|
||||
const new_akhys = getAllUsers.all().length;
|
||||
const diff = new_akhys - initial_akhys;
|
||||
console.log(
|
||||
`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`,
|
||||
);
|
||||
|
||||
const new_akhys = getAllUsers.all().length;
|
||||
const diff = new_akhys - initial_akhys
|
||||
// 2. Fetch Valorant Skins
|
||||
const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]);
|
||||
|
||||
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})`);
|
||||
// Clear and rebuild the in-memory skin cache
|
||||
skins.length = 0;
|
||||
fetchedSkins.forEach((skin) => skins.push(skin));
|
||||
|
||||
// 2. Fetch Valorant Skins
|
||||
const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]);
|
||||
const skinsToInsert = fetchedSkins
|
||||
.filter((skin) => skin.contentTierUuid)
|
||||
.map((skin) => {
|
||||
const tier = fetchedTiers.find((t) => t.uuid === skin.contentTierUuid) || {};
|
||||
const basePrice = calculateBasePrice(skin, tier.rank);
|
||||
return {
|
||||
uuid: skin.uuid,
|
||||
displayName: skin.displayName,
|
||||
contentTierUuid: skin.contentTierUuid,
|
||||
displayIcon: skin.displayIcon,
|
||||
user_id: null,
|
||||
tierRank: tier.rank,
|
||||
tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3",
|
||||
tierText: formatTierText(tier.rank, skin.displayName),
|
||||
basePrice: basePrice.toFixed(0),
|
||||
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0),
|
||||
};
|
||||
});
|
||||
|
||||
// Clear and rebuild the in-memory skin cache
|
||||
skins.length = 0;
|
||||
fetchedSkins.forEach(skin => skins.push(skin));
|
||||
|
||||
const skinsToInsert = fetchedSkins
|
||||
.filter(skin => skin.contentTierUuid)
|
||||
.map(skin => {
|
||||
const tier = fetchedTiers.find(t => t.uuid === skin.contentTierUuid) || {};
|
||||
const basePrice = calculateBasePrice(skin, tier.rank);
|
||||
return {
|
||||
uuid: skin.uuid,
|
||||
displayName: skin.displayName,
|
||||
contentTierUuid: skin.contentTierUuid,
|
||||
displayIcon: skin.displayIcon,
|
||||
user_id: null,
|
||||
tierRank: tier.rank,
|
||||
tierColor: tier.highlightColor?.slice(0, 6) || 'F2F3F3',
|
||||
tierText: formatTierText(tier.rank, skin.displayName),
|
||||
basePrice: basePrice.toFixed(0),
|
||||
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0),
|
||||
};
|
||||
});
|
||||
|
||||
if (skinsToInsert.length > 0) {
|
||||
insertManySkins(skinsToInsert);
|
||||
}
|
||||
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error during initial data sync (getAkhys):', err);
|
||||
}
|
||||
if (skinsToInsert.length > 0) {
|
||||
insertManySkins(skinsToInsert);
|
||||
}
|
||||
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
|
||||
} catch (err) {
|
||||
console.error("Error during initial data sync (getAkhys):", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Cron Jobs / Scheduled Tasks ---
|
||||
|
||||
/**
|
||||
@@ -108,72 +111,87 @@ export async function getAkhys(client) {
|
||||
* @param {object} io - The Socket.IO server instance.
|
||||
*/
|
||||
export function setupCronJobs(client, io) {
|
||||
// Every 10 minutes: Clean up expired interactive sessions
|
||||
cron.schedule('*/10 * * * *', () => {
|
||||
const now = Date.now();
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
// Every 10 minutes: Clean up expired interactive sessions
|
||||
cron.schedule("*/10 * * * *", () => {
|
||||
const now = Date.now();
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const cleanup = (sessions, name) => {
|
||||
let cleanedCount = 0;
|
||||
for (const id in sessions) {
|
||||
if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) {
|
||||
delete sessions[id];
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
|
||||
};
|
||||
const cleanup = (sessions, name) => {
|
||||
let cleanedCount = 0;
|
||||
for (const id in sessions) {
|
||||
if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) {
|
||||
delete sessions[id];
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
|
||||
};
|
||||
|
||||
cleanup(activeInventories, 'inventory');
|
||||
cleanup(activeSearchs, 'search');
|
||||
cleanup(activeInventories, "inventory");
|
||||
cleanup(activeSearchs, "search");
|
||||
|
||||
// Cleanup for predis and poker rooms...
|
||||
// ...
|
||||
});
|
||||
// TODO: Cleanup for predis and poker rooms...
|
||||
// ...
|
||||
});
|
||||
|
||||
// Daily at midnight: Reset daily rewards and init SOTD
|
||||
cron.schedule('0 0 * * *', async () => {
|
||||
console.log('[Cron] Running daily midnight tasks...');
|
||||
try {
|
||||
resetDailyReward.run();
|
||||
console.log('[Cron] Daily rewards have been reset for all users.');
|
||||
//if (!getSOTD.get()) {
|
||||
initTodaysSOTD();
|
||||
//}
|
||||
} catch (e) {
|
||||
console.error('[Cron] Error during daily reset:', e);
|
||||
}
|
||||
});
|
||||
// Daily at midnight: Reset daily rewards and init SOTD
|
||||
cron.schedule("0 0 * * *", async () => {
|
||||
console.log("[Cron] Running daily midnight tasks...");
|
||||
try {
|
||||
resetDailyReward.run();
|
||||
console.log("[Cron] Daily rewards have been reset for all users.");
|
||||
//if (!getSOTD.get()) {
|
||||
initTodaysSOTD();
|
||||
//}
|
||||
} catch (e) {
|
||||
console.error("[Cron] Error during daily reset:", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Daily at 7 AM: Re-sync users and skins
|
||||
cron.schedule('0 7 * * *', async () => {
|
||||
console.log('[Cron] Running daily 7 AM data sync...');
|
||||
await getAkhys(client);
|
||||
});
|
||||
// Daily at 7 AM: Re-sync users and skins
|
||||
cron.schedule("0 7 * * *", async () => {
|
||||
console.log("[Cron] Running daily 7 AM data sync...");
|
||||
await getAkhys(client);
|
||||
try {
|
||||
const akhys = getAllAkhys.all();
|
||||
for (const akhy of akhys) {
|
||||
const user = await client.users.cache.get(akhy.id);
|
||||
try {
|
||||
updateUserAvatar.run({
|
||||
id: akhy.id,
|
||||
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Cron] Error during daily avatar update:", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --- Formatting Helpers ---
|
||||
|
||||
export function capitalize(str) {
|
||||
if (typeof str !== 'string' || str.length === 0) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
if (typeof str !== "string" || str.length === 0) return "";
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function formatTime(seconds) {
|
||||
const d = Math.floor(seconds / (3600*24));
|
||||
const h = Math.floor(seconds % (3600*24) / 3600);
|
||||
const m = Math.floor(seconds % 3600 / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const d = Math.floor(seconds / (3600 * 24));
|
||||
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
const parts = [];
|
||||
if (d > 0) parts.push(`**${d}** jour${d > 1 ? 's' : ''}`);
|
||||
if (h > 0) parts.push(`**${h}** heure${h > 1 ? 's' : ''}`);
|
||||
if (m > 0) parts.push(`**${m}** minute${m > 1 ? 's' : ''}`);
|
||||
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? 's' : ''}`);
|
||||
const parts = [];
|
||||
if (d > 0) parts.push(`**${d}** jour${d > 1 ? "s" : ""}`);
|
||||
if (h > 0) parts.push(`**${h}** heure${h > 1 ? "s" : ""}`);
|
||||
if (m > 0) parts.push(`**${m}** minute${m > 1 ? "s" : ""}`);
|
||||
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? "s" : ""}`);
|
||||
|
||||
return parts.join(', ').replace(/,([^,]*)$/, ' et$1');
|
||||
return parts.join(", ").replace(/,([^,]*)$/, " et$1");
|
||||
}
|
||||
|
||||
// --- External API Helpers ---
|
||||
@@ -182,15 +200,15 @@ export function formatTime(seconds) {
|
||||
* Fetches user data from the "APO" service.
|
||||
*/
|
||||
export async function getAPOUsers() {
|
||||
const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`;
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching APO users:', error);
|
||||
return null;
|
||||
}
|
||||
const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`;
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error fetching APO users:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,112 +217,124 @@ export async function getAPOUsers() {
|
||||
* @param {number} amount - The amount to "buy".
|
||||
*/
|
||||
export async function postAPOBuy(userId, amount) {
|
||||
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
|
||||
return fetch(fetchUrl, { method: 'POST' });
|
||||
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
|
||||
return fetch(fetchUrl, { method: "POST" });
|
||||
}
|
||||
|
||||
|
||||
// --- Miscellaneous Helpers ---
|
||||
|
||||
export async function getOnlineUsersWithRole(guild, roleId) {
|
||||
if (!guild || !roleId) return new Map();
|
||||
try {
|
||||
const members = await guild.members.fetch();
|
||||
return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.presence?.status !== undefined && m.roles.cache.has(roleId));
|
||||
} catch (err) {
|
||||
console.error('Error fetching online members with role:', err);
|
||||
return new Map();
|
||||
}
|
||||
if (!guild || !roleId) return new Map();
|
||||
try {
|
||||
const members = await guild.members.fetch();
|
||||
return members.filter(
|
||||
(m) =>
|
||||
!m.user.bot &&
|
||||
m.presence?.status !== "offline" &&
|
||||
m.presence?.status !== undefined &&
|
||||
m.roles.cache.has(roleId),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error fetching online members with role:", err);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
export function getRandomEmoji(list = 0) {
|
||||
const emojiLists = [
|
||||
['😭','😄','😌','🤓','😎','😤','🤖','😶🌫️','🌏','📸','💿','👋','🌊','✨'],
|
||||
['<:CAUGHT:1323810730155446322>', '<:hinhinhin:1072510144933531758>', '<:o7:1290773422451986533>', '<:zhok:1115221772623683686>', '<:nice:1154049521110765759>', '<:nerd:1087658195603951666>', '<:peepSelfie:1072508131839594597>'],
|
||||
];
|
||||
const selectedList = emojiLists[list] || [''];
|
||||
return selectedList[Math.floor(Math.random() * selectedList.length)];
|
||||
const emojiLists = [
|
||||
["😭", "😄", "😌", "🤓", "😎", "😤", "🤖", "😶🌫️", "🌏", "📸", "💿", "👋", "🌊", "✨"],
|
||||
[
|
||||
"<:CAUGHT:1323810730155446322>",
|
||||
"<:hinhinhin:1072510144933531758>",
|
||||
"<:o7:1290773422451986533>",
|
||||
"<:zhok:1115221772623683686>",
|
||||
"<:nice:1154049521110765759>",
|
||||
"<:nerd:1087658195603951666>",
|
||||
"<:peepSelfie:1072508131839594597>",
|
||||
],
|
||||
];
|
||||
const selectedList = emojiLists[list] || [""];
|
||||
return selectedList[Math.floor(Math.random() * selectedList.length)];
|
||||
}
|
||||
|
||||
export function formatAmount(amount) {
|
||||
if (amount >= 1000000000) {
|
||||
amount /= 1000000000
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'Md'
|
||||
)
|
||||
}
|
||||
if (amount >= 1000000) {
|
||||
amount /= 1000000
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'M'
|
||||
)
|
||||
}
|
||||
if (amount >= 10000) {
|
||||
amount /= 1000
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'K'
|
||||
)
|
||||
}
|
||||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
|
||||
if (amount >= 1000000000) {
|
||||
amount /= 1000000000;
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "Md"
|
||||
);
|
||||
}
|
||||
if (amount >= 1000000) {
|
||||
amount /= 1000000;
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "M"
|
||||
);
|
||||
}
|
||||
if (amount >= 10000) {
|
||||
amount /= 1000;
|
||||
return (
|
||||
amount
|
||||
.toFixed(2)
|
||||
.toString()
|
||||
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "K"
|
||||
);
|
||||
}
|
||||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
}
|
||||
|
||||
|
||||
// --- Private Helpers ---
|
||||
|
||||
export function calculateBasePrice(skin, tierRank) {
|
||||
const name = skin.displayName.toLowerCase();
|
||||
let price = 6000; // Default for melee
|
||||
if (name.includes('classic')) price = 150;
|
||||
else if (name.includes('shorty')) price = 300;
|
||||
else if (name.includes('frenzy')) price = 450;
|
||||
else if (name.includes('ghost')) price = 500;
|
||||
else if (name.includes('sheriff')) price = 800;
|
||||
else if (name.includes('stinger')) price = 1000;
|
||||
else if (name.includes('spectre')) price = 1600;
|
||||
else if (name.includes('bucky')) price = 900;
|
||||
else if (name.includes('judge')) price = 1500;
|
||||
else if (name.includes('bulldog')) price = 2100;
|
||||
else if (name.includes('guardian')) price = 2700
|
||||
else if (name.includes('vandal') || name.includes('phantom')) price = 2900;
|
||||
else if (name.includes('marshal')) price = 950;
|
||||
else if (name.includes('outlaw')) price = 2400;
|
||||
else if (name.includes('operator')) price = 4500;
|
||||
else if (name.includes('ares')) price = 1700;
|
||||
else if (name.includes('odin')) price = 3200;
|
||||
const name = skin.displayName.toLowerCase();
|
||||
let price = 6000; // Default for melee
|
||||
if (name.includes("classic")) price = 150;
|
||||
else if (name.includes("shorty")) price = 300;
|
||||
else if (name.includes("frenzy")) price = 450;
|
||||
else if (name.includes("ghost")) price = 500;
|
||||
else if (name.includes("sheriff")) price = 800;
|
||||
else if (name.includes("stinger")) price = 1000;
|
||||
else if (name.includes("spectre")) price = 1600;
|
||||
else if (name.includes("bucky")) price = 900;
|
||||
else if (name.includes("judge")) price = 1500;
|
||||
else if (name.includes("bulldog")) price = 2100;
|
||||
else if (name.includes("guardian")) price = 2700;
|
||||
else if (name.includes("vandal") || name.includes("phantom")) price = 2900;
|
||||
else if (name.includes("marshal")) price = 950;
|
||||
else if (name.includes("outlaw")) price = 2400;
|
||||
else if (name.includes("operator")) price = 4500;
|
||||
else if (name.includes("ares")) price = 1700;
|
||||
else if (name.includes("odin")) price = 3200;
|
||||
|
||||
price *= (1 + (tierRank || 0));
|
||||
if (name.includes('vct')) price *= 1.25;
|
||||
if (name.includes('champions')) price *= 2;
|
||||
price *= 1 + (tierRank || 0);
|
||||
if (name.includes("vct")) price *= 1.25;
|
||||
if (name.includes("champions")) price *= 2;
|
||||
|
||||
return price / 124;
|
||||
return price / 124;
|
||||
}
|
||||
|
||||
export function calculateMaxPrice(basePrice, skin) {
|
||||
let res = basePrice;
|
||||
res *= (1 + (skin.levels.length / Math.max(skin.levels.length, 2)));
|
||||
res *= (1 + (skin.chromas.length / 4));
|
||||
return res;
|
||||
let res = basePrice;
|
||||
res *= 1 + skin.levels.length / Math.max(skin.levels.length, 2);
|
||||
res *= 1 + skin.chromas.length / 4;
|
||||
return res;
|
||||
}
|
||||
|
||||
function formatTierText(rank, displayName) {
|
||||
const tiers = {
|
||||
0: '**<:select:1362964319498670222> Select**',
|
||||
1: '**<:deluxe:1362964308094488797> Deluxe**',
|
||||
2: '**<:premium:1362964330349330703> Premium**',
|
||||
3: '**<:exclusive:1362964427556651098> Exclusive**',
|
||||
4: '**<:ultra:1362964339685986314> Ultra**',
|
||||
};
|
||||
let res = tiers[rank] || 'Pas de tier';
|
||||
if (displayName.includes('VCT')) res += ' | Esports';
|
||||
if (displayName.toLowerCase().includes('champions')) res += ' | Champions';
|
||||
return res;
|
||||
const tiers = {
|
||||
0: "**<:select:1362964319498670222> Select**",
|
||||
1: "**<:deluxe:1362964308094488797> Deluxe**",
|
||||
2: "**<:premium:1362964330349330703> Premium**",
|
||||
3: "**<:exclusive:1362964427556651098> Exclusive**",
|
||||
4: "**<:ultra:1362964339685986314> Ultra**",
|
||||
};
|
||||
let res = tiers[rank] || "Pas de tier";
|
||||
if (displayName.includes("VCT")) res += " | Esports";
|
||||
if (displayName.toLowerCase().includes("champions")) res += " | Champions";
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user