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">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="EditorConfigDeprecatedDescriptor" enabled="true" level="WARNING" enabled_by_default="true" />
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
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
|
# FLOPOBOT DEUXIEME DU NOM
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
Below is a basic overview of the project structure:
|
Below is a basic overview of the project structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -15,5 +16,6 @@ Below is a basic overview of the project structure:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## FlopoSite
|
## FlopoSite
|
||||||
|
|
||||||
- FlopoBot has its own website to use it a different way
|
- FlopoBot has its own website to use it a different way
|
||||||
[FlopoSite's repo](https://github.com/cassoule/floposite)
|
[FlopoSite's repo](https://github.com/cassoule/floposite)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { registerCommands } from './src/config/commands.js';
|
import { registerCommands } from "./src/config/commands.js";
|
||||||
|
|
||||||
console.log('Registering global commands...');
|
console.log("Registering global commands...");
|
||||||
registerCommands();
|
registerCommands();
|
||||||
console.log('Commands registered.');
|
console.log("Commands registered.");
|
||||||
|
|||||||
9
eslint.config.js
Normal file
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" },
|
||||||
|
]);
|
||||||
56
index.js
56
index.js
@@ -1,53 +1,51 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import http from 'http';
|
import http from "http";
|
||||||
import { Server } from 'socket.io';
|
import { Server } from "socket.io";
|
||||||
|
|
||||||
import { app } from './src/server/app.js';
|
import { app } from "./src/server/app.js";
|
||||||
import { client } from './src/bot/client.js';
|
import { client } from "./src/bot/client.js";
|
||||||
import { initializeEvents } from './src/bot/events.js';
|
import { initializeEvents } from "./src/bot/events.js";
|
||||||
import { initializeSocket } from './src/server/socket.js';
|
import { initializeSocket } from "./src/server/socket.js";
|
||||||
import { getAkhys, setupCronJobs } from './src/utils/index.js';
|
import { setupCronJobs } from "./src/utils/index.js";
|
||||||
|
|
||||||
// --- SERVER INITIALIZATION ---
|
// --- SERVER INITIALIZATION ---
|
||||||
const PORT = process.env.PORT || 25578;
|
const PORT = process.env.PORT || 25578;
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
// --- SOCKET.IO INITIALIZATION ---
|
// --- SOCKET.IO INITIALIZATION ---
|
||||||
const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||||
export const io = new Server(server, {
|
export const io = new Server(server, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: FLAPI_URL,
|
origin: FLAPI_URL,
|
||||||
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
|
methods: ["GET", "POST", "PUT", "OPTIONS"],
|
||||||
},
|
},
|
||||||
pingInterval: 5000,
|
pingInterval: 5000,
|
||||||
pingTimeout: 5000,
|
pingTimeout: 5000,
|
||||||
});
|
});
|
||||||
initializeSocket(io, client);
|
initializeSocket(io, client);
|
||||||
|
|
||||||
|
|
||||||
// --- BOT INITIALIZATION ---
|
// --- BOT INITIALIZATION ---
|
||||||
initializeEvents(client, io);
|
initializeEvents(client, io);
|
||||||
client.login(process.env.BOT_TOKEN).then(() => {
|
client.login(process.env.BOT_TOKEN).then(() => {
|
||||||
console.log(`Logged in as ${client.user.tag}`);
|
console.log(`Logged in as ${client.user.tag}`);
|
||||||
console.log('[Discord Bot Events Initialized]');
|
console.log("[Discord Bot Events Initialized]");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- APP STARTUP ---
|
// --- APP STARTUP ---
|
||||||
server.listen(PORT, async () => {
|
server.listen(PORT, async () => {
|
||||||
console.log(`Express+Socket.IO server listening on port ${PORT}`);
|
console.log(`Express+Socket.IO server listening on port ${PORT}`);
|
||||||
console.log(`[Connected with ${FLAPI_URL}]`);
|
console.log(`[Connected with ${FLAPI_URL}]`);
|
||||||
|
|
||||||
// Initial data fetch and setup
|
// Initial data fetch and setup
|
||||||
try {
|
/*try {
|
||||||
await getAkhys(client);
|
await getAkhys(client);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Initial Fetch Error');
|
console.log('Initial Fetch Error');
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Setup scheduled tasks
|
// Setup scheduled tasks
|
||||||
//setupCronJobs(client, io);
|
setupCronJobs(client, io);
|
||||||
console.log('[Cron Jobs Initialized]');
|
console.log("[Cron Jobs Initialized]");
|
||||||
|
|
||||||
console.log('--- FlopoBOT is ready ---');
|
console.log("--- FlopoBOT is ready ---");
|
||||||
});
|
});
|
||||||
|
|||||||
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",
|
"name": "t12_flopobot",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Flopobot le 2 donc en mieux",
|
"description": "Flopobot le 2 donc en mieux",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.x"
|
"node": ">=18.x"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"register": "node commands.js",
|
"register": "node commands.js",
|
||||||
"dev": "nodemon index.js"
|
"dev": "nodemon index.js"
|
||||||
},
|
},
|
||||||
"author": "Milo Gourvest",
|
"author": "Milo Gourvest",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^0.8.0",
|
"@google/genai": "^0.8.0",
|
||||||
"@mistralai/mistralai": "^1.6.0",
|
"@mistralai/mistralai": "^1.6.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"better-sqlite3": "^11.9.1",
|
"better-sqlite3": "^11.9.1",
|
||||||
"discord-interactions": "^4.0.0",
|
"discord-interactions": "^4.0.0",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"openai": "^4.104.0",
|
"openai": "^4.104.0",
|
||||||
"pokersolver": "^2.1.4",
|
"pokersolver": "^2.1.4",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.0"
|
"@eslint/json": "^0.14.0",
|
||||||
}
|
"eslint": "^9.39.1",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"nodemon": "^3.0.0",
|
||||||
|
"prettier": "3.6.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": ["config:recommended", ":disableDependencyDashboard", ":preserveSemverRanges"],
|
||||||
"config:recommended",
|
"ignorePaths": ["**/node_modules/**"]
|
||||||
":disableDependencyDashboard",
|
|
||||||
":preserveSemverRanges"
|
|
||||||
],
|
|
||||||
"ignorePaths": [
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic function for making requests to the Discord API.
|
* A generic function for making requests to the Discord API.
|
||||||
@@ -10,38 +10,38 @@ import 'dotenv/config';
|
|||||||
* @throws Will throw an error if the API request is not successful.
|
* @throws Will throw an error if the API request is not successful.
|
||||||
*/
|
*/
|
||||||
export async function DiscordRequest(endpoint, options) {
|
export async function DiscordRequest(endpoint, options) {
|
||||||
// Construct the full API URL
|
// Construct the full API URL
|
||||||
const url = 'https://discord.com/api/v10/' + endpoint;
|
const url = "https://discord.com/api/v10/" + endpoint;
|
||||||
|
|
||||||
// Stringify the payload if it exists
|
// Stringify the payload if it exists
|
||||||
if (options && options.body) {
|
if (options && options.body) {
|
||||||
options.body = JSON.stringify(options.body);
|
options.body = JSON.stringify(options.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fetch to make the request, automatically including required headers
|
// Use fetch to make the request, automatically including required headers
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bot ${process.env.DISCORD_TOKEN}`,
|
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)',
|
"User-Agent": "DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)",
|
||||||
},
|
},
|
||||||
...options, // Spread the given options (e.g., method, body)
|
...options, // Spread the given options (e.g., method, body)
|
||||||
});
|
});
|
||||||
|
|
||||||
// If the request was not successful, throw a detailed error
|
// If the request was not successful, throw a detailed error
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
let data
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
data = res;
|
data = res;
|
||||||
}
|
}
|
||||||
console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data);
|
console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data);
|
||||||
throw new Error(JSON.stringify(data));
|
throw new Error(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the original response object for further processing
|
// Return the original response object for further processing
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,15 +51,15 @@ export async function DiscordRequest(endpoint, options) {
|
|||||||
* @param {Array<object>} commands - An array of command objects to install.
|
* @param {Array<object>} commands - An array of command objects to install.
|
||||||
*/
|
*/
|
||||||
export async function InstallGlobalCommands(appId, commands) {
|
export async function InstallGlobalCommands(appId, commands) {
|
||||||
// API endpoint for bulk overwriting global commands
|
// API endpoint for bulk overwriting global commands
|
||||||
const endpoint = `applications/${appId}/commands`;
|
const endpoint = `applications/${appId}/commands`;
|
||||||
|
|
||||||
console.log('Installing global commands...');
|
console.log("Installing global commands...");
|
||||||
try {
|
try {
|
||||||
// This uses the generic DiscordRequest function to make the API call
|
// This uses the generic DiscordRequest function to make the API call
|
||||||
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
|
await DiscordRequest(endpoint, { method: "PUT", body: commands });
|
||||||
console.log('Successfully installed global commands.');
|
console.log("Successfully installed global commands.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error installing global commands:', err);
|
console.error("Error installing global commands:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export async function getValorantSkins(locale='fr-FR') {
|
export async function getValorantSkins(locale = "fr-FR") {
|
||||||
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' });
|
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: "GET" });
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.data
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSkinTiers(locale='fr-FR') {
|
export async function getSkinTiers(locale = "fr-FR") {
|
||||||
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'});
|
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: "GET" });
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.data
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
* 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.
|
* It is configured with all the necessary intents to receive the events it needs.
|
||||||
*/
|
*/
|
||||||
export const client = new Client({
|
export const client = new Client({
|
||||||
// Define the events the bot needs to receive from Discord's gateway.
|
// Define the events the bot needs to receive from Discord's gateway.
|
||||||
intents: [
|
intents: [
|
||||||
// Required for basic guild information and events.
|
// Required for basic guild information and events.
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
|
|
||||||
// Required to receive messages in guilds (e.g., in #general).
|
// Required to receive messages in guilds (e.g., in #general).
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
|
|
||||||
// A PRIVILEGED INTENT, required to read the content of messages.
|
// A PRIVILEGED INTENT, required to read the content of messages.
|
||||||
// This is necessary for the AI handler, admin commands, and "quoi/feur".
|
// This is necessary for the AI handler, admin commands, and "quoi/feur".
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
|
|
||||||
// Required to receive updates when members join, leave, or are updated.
|
// Required to receive updates when members join, leave, or are updated.
|
||||||
// Crucial for fetching member details for commands like /timeout or /info.
|
// Crucial for fetching member details for commands like /timeout or /info.
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.GuildMembers,
|
||||||
|
|
||||||
// Required to receive member presence updates (online, idle, offline).
|
// Required to receive member presence updates (online, idle, offline).
|
||||||
// Necessary for features like `getOnlineUsersWithRole`.
|
// Necessary for features like `getOnlineUsersWithRole`.
|
||||||
GatewayIntentBits.GuildPresences,
|
GatewayIntentBits.GuildPresences,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { InteractionResponseType, MessageComponentTypes, ButtonStyleTypes } from "discord-interactions";
|
||||||
InteractionResponseType,
|
|
||||||
MessageComponentTypes,
|
|
||||||
ButtonStyleTypes,
|
|
||||||
} from 'discord-interactions';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /floposite slash command.
|
* Handles the /floposite slash command.
|
||||||
@@ -11,45 +7,45 @@ import {
|
|||||||
* @param {object} res - The Express response object.
|
* @param {object} res - The Express response object.
|
||||||
*/
|
*/
|
||||||
export async function handleFlopoSiteCommand(req, res) {
|
export async function handleFlopoSiteCommand(req, res) {
|
||||||
// The URL for the link button. Consider moving to .env if it changes.
|
// The URL for the link button. Consider moving to .env if it changes.
|
||||||
const siteUrl = process.env.FLOPOSITE_URL || 'https://floposite.com';
|
const siteUrl = process.env.FLOPOSITE_URL || "https://floposite.com";
|
||||||
|
|
||||||
// The URL for the thumbnail image.
|
// The URL for the thumbnail image.
|
||||||
const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`;
|
const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`;
|
||||||
|
|
||||||
// Define the components (the link button)
|
// Define the components (the link button)
|
||||||
const components = [
|
const components = [
|
||||||
{
|
{
|
||||||
type: MessageComponentTypes.ACTION_ROW,
|
type: MessageComponentTypes.ACTION_ROW,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: MessageComponentTypes.BUTTON,
|
type: MessageComponentTypes.BUTTON,
|
||||||
label: 'Aller sur FlopoSite',
|
label: "Aller sur FlopoSite",
|
||||||
style: ButtonStyleTypes.LINK,
|
style: ButtonStyleTypes.LINK,
|
||||||
url: siteUrl,
|
url: siteUrl,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define the embed message
|
// Define the embed message
|
||||||
const embeds = [
|
const embeds = [
|
||||||
{
|
{
|
||||||
title: 'FlopoSite',
|
title: "FlopoSite",
|
||||||
description: "L'officiel et très goatesque site de FlopoBot.",
|
description: "L'officiel et très goatesque site de FlopoBot.",
|
||||||
color: 0x6571F3, // A custom blue color
|
color: 0x6571f3, // A custom blue color
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: thumbnailUrl,
|
url: thumbnailUrl,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Send the response to Discord
|
// Send the response to Discord
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: embeds,
|
embeds: embeds,
|
||||||
components: components,
|
components: components,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InteractionResponseType } from 'discord-interactions';
|
import { InteractionResponseType } from "discord-interactions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /info slash command.
|
* Handles the /info slash command.
|
||||||
@@ -8,64 +8,61 @@ import { InteractionResponseType } from 'discord-interactions';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleInfoCommand(req, res, client) {
|
export async function handleInfoCommand(req, res, client) {
|
||||||
const { guild_id } = req.body;
|
const { guild_id } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the guild object from the client
|
// Fetch the guild object from the client
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const guild = await client.guilds.fetch(guild_id);
|
||||||
|
|
||||||
// Fetch all members to ensure the cache is up to date
|
// Fetch all members to ensure the cache is up to date
|
||||||
await guild.members.fetch();
|
await guild.members.fetch();
|
||||||
|
|
||||||
// Filter the cached members to find those who are timed out
|
// Filter the cached members to find those who are timed out
|
||||||
// A member is timed out if their `communicationDisabledUntil` property is a future date.
|
// A member is timed out if their `communicationDisabledUntil` property is a future date.
|
||||||
const timedOutMembers = guild.members.cache.filter(
|
const timedOutMembers = guild.members.cache.filter(
|
||||||
(member) =>
|
(member) => member.communicationDisabledUntilTimestamp && member.communicationDisabledUntilTimestamp > Date.now(),
|
||||||
member.communicationDisabledUntilTimestamp &&
|
);
|
||||||
member.communicationDisabledUntilTimestamp > Date.now()
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Case 1: No members are timed out ---
|
// --- Case 1: No members are timed out ---
|
||||||
if (timedOutMembers.size === 0) {
|
if (timedOutMembers.size === 0) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: 'Membres Timeout',
|
title: "Membres Timeout",
|
||||||
description: "Aucun membre n'est actuellement timeout.",
|
description: "Aucun membre n'est actuellement timeout.",
|
||||||
color: 0x4F545C, // Discord's gray color
|
color: 0x4f545c, // Discord's gray color
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Case 2: At least one member is timed out ---
|
// --- Case 2: At least one member is timed out ---
|
||||||
// Format the list of timed-out members for the embed
|
// Format the list of timed-out members for the embed
|
||||||
const memberList = timedOutMembers
|
const memberList = timedOutMembers
|
||||||
.map((member) => {
|
.map((member) => {
|
||||||
// toLocaleString provides a user-friendly date and time format
|
// toLocaleString provides a user-friendly date and time format
|
||||||
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString('fr-FR');
|
const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString("fr-FR");
|
||||||
return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`;
|
return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`;
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join("\n");
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: 'Membres Actuellement Timeout',
|
title: "Membres Actuellement Timeout",
|
||||||
description: memberList,
|
description: memberList,
|
||||||
color: 0xED4245, // Discord's red color
|
color: 0xed4245, // Discord's red color
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error handling /info command:", error);
|
||||||
console.error('Error handling /info command:', error);
|
return res.status(500).json({ error: "Failed to fetch timeout information." });
|
||||||
return res.status(500).json({ error: 'Failed to fetch timeout information.' });
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
import { activeInventories, skins } from '../../game/state.js';
|
import { activeInventories, skins } from "../../game/state.js";
|
||||||
import { getUserInventory } from '../../database/index.js';
|
import { getUserInventory } from "../../database/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /inventory slash command.
|
* Handles the /inventory slash command.
|
||||||
@@ -17,122 +17,149 @@ import { getUserInventory } from '../../database/index.js';
|
|||||||
* @param {string} interactionId - The unique ID of the interaction.
|
* @param {string} interactionId - The unique ID of the interaction.
|
||||||
*/
|
*/
|
||||||
export async function handleInventoryCommand(req, res, client, interactionId) {
|
export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||||
const { member, guild_id, token, data } = req.body;
|
const { member, guild_id, token, data } = req.body;
|
||||||
const commandUserId = member.user.id;
|
const commandUserId = member.user.id;
|
||||||
// User can specify another member, otherwise it defaults to themself
|
// User can specify another member, otherwise it defaults to themself
|
||||||
const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId;
|
const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- 1. Fetch Data ---
|
// --- 1. Fetch Data ---
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const guild = await client.guilds.fetch(guild_id);
|
||||||
const targetMember = await guild.members.fetch(targetUserId);
|
const targetMember = await guild.members.fetch(targetUserId);
|
||||||
const inventorySkins = getUserInventory.all({ user_id: targetUserId });
|
const inventorySkins = getUserInventory.all({ user_id: targetUserId });
|
||||||
|
|
||||||
// --- 2. Handle Empty Inventory ---
|
// --- 2. Handle Empty Inventory ---
|
||||||
if (inventorySkins.length === 0) {
|
if (inventorySkins.length === 0) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
{
|
||||||
description: "Cet inventaire est vide.",
|
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||||
color: 0x4F545C, // Discord Gray
|
description: "Cet inventaire est vide.",
|
||||||
}],
|
color: 0x4f545c, // Discord Gray
|
||||||
},
|
},
|
||||||
});
|
],
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- 3. Store Interactive Session State ---
|
// --- 3. Store Interactive Session State ---
|
||||||
// This state is crucial for the component handlers to know which inventory to update.
|
// This state is crucial for the component handlers to know which inventory to update.
|
||||||
activeInventories[interactionId] = {
|
activeInventories[interactionId] = {
|
||||||
akhyId: targetUserId, // The inventory owner
|
akhyId: targetUserId, // The inventory owner
|
||||||
userId: commandUserId, // The user who ran the command
|
userId: commandUserId, // The user who ran the command
|
||||||
page: 0,
|
page: 0,
|
||||||
amount: inventorySkins.length,
|
amount: inventorySkins.length,
|
||||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn
|
inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 4. Prepare Embed Content ---
|
// --- 4. Prepare Embed Content ---
|
||||||
const currentSkin = inventorySkins[0];
|
const currentSkin = inventorySkins[0];
|
||||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||||
if (!skinData) {
|
if (!skinData) {
|
||||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||||
}
|
}
|
||||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||||
|
|
||||||
// --- Helper functions for formatting ---
|
// --- Helper functions for formatting ---
|
||||||
const getChromaText = (skin, skinInfo) => {
|
const getChromaText = (skin, skinInfo) => {
|
||||||
let result = "";
|
let result = "";
|
||||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||||
result += skin.currentChroma === i ? '💠 ' : '◾ ';
|
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||||
}
|
}
|
||||||
return result || 'N/A';
|
return result || "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChromaName = (skin, skinInfo) => {
|
const getChromaName = (skin, skinInfo) => {
|
||||||
if (skin.currentChroma > 1) {
|
if (skin.currentChroma > 1) {
|
||||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName.replace(/[\r\n]+/g, ' ').replace(skinInfo.displayName, '').trim();
|
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName
|
||||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
.replace(/[\r\n]+/g, " ")
|
||||||
return match ? match[1].trim() : name;
|
.replace(skinInfo.displayName, "")
|
||||||
}
|
.trim();
|
||||||
return 'Base';
|
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||||
};
|
return match ? match[1].trim() : name;
|
||||||
|
}
|
||||||
|
return "Base";
|
||||||
|
};
|
||||||
|
|
||||||
const getImageUrl = (skin, skinInfo) => {
|
const getImageUrl = (skin, skinInfo) => {
|
||||||
if (skin.currentLvl === skinInfo.levels.length) {
|
if (skin.currentLvl === skinInfo.levels.length) {
|
||||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||||
}
|
}
|
||||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 5. Build Initial Components (Buttons) ---
|
// --- 5. Build Initial Components (Buttons) ---
|
||||||
const components = [
|
const components = [
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
{
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
type: MessageComponentTypes.BUTTON,
|
||||||
];
|
custom_id: `prev_page_${interactionId}`,
|
||||||
|
label: "⏮️ Préc.",
|
||||||
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MessageComponentTypes.BUTTON,
|
||||||
|
custom_id: `next_page_${interactionId}`,
|
||||||
|
label: "Suiv. ⏭️",
|
||||||
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
const isUpgradable =
|
||||||
// Only show upgrade button if the skin is upgradable AND the command user owns the inventory
|
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||||
if (isUpgradable && targetUserId === commandUserId) {
|
// Only show upgrade button if the skin is upgradable AND the command user owns the inventory
|
||||||
components.push({
|
if (isUpgradable && targetUserId === commandUserId) {
|
||||||
type: MessageComponentTypes.BUTTON,
|
components.push({
|
||||||
custom_id: `upgrade_${interactionId}`,
|
type: MessageComponentTypes.BUTTON,
|
||||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`,
|
custom_id: `upgrade_${interactionId}`,
|
||||||
style: ButtonStyleTypes.PRIMARY,
|
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||||
});
|
style: ButtonStyleTypes.PRIMARY,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- 6. Send Final Response ---
|
// --- 6. Send Final Response ---
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
{
|
||||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||||
footer: { text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` },
|
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||||
fields: [{
|
footer: {
|
||||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
|
||||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
},
|
||||||
}],
|
fields: [
|
||||||
image: { url: getImageUrl(currentSkin, skinData) },
|
{
|
||||||
}],
|
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||||
components: [{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||||
{ type: MessageComponentTypes.ACTION_ROW,
|
},
|
||||||
components: [{
|
],
|
||||||
type: MessageComponentTypes.BUTTON,
|
image: { url: getImageUrl(currentSkin, skinData) },
|
||||||
url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`,
|
},
|
||||||
label: 'Voir sur FlopoSite',
|
],
|
||||||
style: ButtonStyleTypes.LINK,}]
|
components: [
|
||||||
}],
|
{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||||
},
|
{
|
||||||
});
|
type: MessageComponentTypes.ACTION_ROW,
|
||||||
|
components: [
|
||||||
} catch (error) {
|
{
|
||||||
console.error('Error handling /inventory command:', error);
|
type: MessageComponentTypes.BUTTON,
|
||||||
return res.status(500).json({ error: 'Failed to generate inventory.' });
|
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 {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
import { activeSearchs, skins } from '../../game/state.js';
|
import { activeSearchs, skins } from "../../game/state.js";
|
||||||
import { getAllSkins } from '../../database/index.js';
|
import { getAllSkins } from "../../database/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /search slash command.
|
* Handles the /search slash command.
|
||||||
@@ -17,106 +17,117 @@ import { getAllSkins } from '../../database/index.js';
|
|||||||
* @param {string} interactionId - The unique ID of the interaction.
|
* @param {string} interactionId - The unique ID of the interaction.
|
||||||
*/
|
*/
|
||||||
export async function handleSearchCommand(req, res, client, interactionId) {
|
export async function handleSearchCommand(req, res, client, interactionId) {
|
||||||
const { member, guild_id, token, data } = req.body;
|
const { member, guild_id, token, data } = req.body;
|
||||||
const userId = member.user.id;
|
const userId = member.user.id;
|
||||||
const searchValue = data.options[0].value.toLowerCase();
|
const searchValue = data.options[0].value.toLowerCase();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- 1. Fetch and Filter Data ---
|
// --- 1. Fetch and Filter Data ---
|
||||||
const allDbSkins = getAllSkins.all();
|
const allDbSkins = getAllSkins.all();
|
||||||
const resultSkins = allDbSkins.filter((skin) =>
|
const resultSkins = allDbSkins.filter(
|
||||||
skin.displayName.toLowerCase().includes(searchValue) ||
|
(skin) =>
|
||||||
skin.tierText.toLowerCase().includes(searchValue)
|
skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- 2. Handle No Results ---
|
// --- 2. Handle No Results ---
|
||||||
if (resultSkins.length === 0) {
|
if (resultSkins.length === 0) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: 'Aucun skin ne correspond à votre recherche.',
|
content: "Aucun skin ne correspond à votre recherche.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Store Interactive Session State ---
|
// --- 3. Store Interactive Session State ---
|
||||||
activeSearchs[interactionId] = {
|
activeSearchs[interactionId] = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
page: 0,
|
page: 0,
|
||||||
amount: resultSkins.length,
|
amount: resultSkins.length,
|
||||||
resultSkins: resultSkins,
|
resultSkins: resultSkins,
|
||||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
searchValue: searchValue,
|
searchValue: searchValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 4. Prepare Initial Embed Content ---
|
// --- 4. Prepare Initial Embed Content ---
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const guild = await client.guilds.fetch(guild_id);
|
||||||
const currentSkin = resultSkins[0];
|
const currentSkin = resultSkins[0];
|
||||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||||
if (!skinData) {
|
if (!skinData) {
|
||||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch owner details if the skin is owned
|
// Fetch owner details if the skin is owned
|
||||||
let ownerText = '';
|
let ownerText = "";
|
||||||
if (currentSkin.user_id) {
|
if (currentSkin.user_id) {
|
||||||
try {
|
try {
|
||||||
const owner = await guild.members.fetch(currentSkin.user_id);
|
const owner = await guild.members.fetch(currentSkin.user_id);
|
||||||
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
|
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||||
ownerText = '| Appartenant à un utilisateur inconnu';
|
ownerText = "| Appartenant à un utilisateur inconnu";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get the best possible image for the skin
|
// Helper to get the best possible image for the skin
|
||||||
const getImageUrl = (skinInfo) => {
|
const getImageUrl = (skinInfo) => {
|
||||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||||
|
|
||||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
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 ---
|
// --- 5. Build Initial Components & Embed ---
|
||||||
const components = [
|
const components = [
|
||||||
{
|
{
|
||||||
type: MessageComponentTypes.ACTION_ROW,
|
type: MessageComponentTypes.ACTION_ROW,
|
||||||
components: [
|
components: [
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_search_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
{
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_search_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
type: MessageComponentTypes.BUTTON,
|
||||||
],
|
custom_id: `prev_search_page_${interactionId}`,
|
||||||
},
|
label: "⏮️ Préc.",
|
||||||
];
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MessageComponentTypes.BUTTON,
|
||||||
|
custom_id: `next_search_page_${interactionId}`,
|
||||||
|
label: "Suiv. ⏭️",
|
||||||
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const embed = {
|
const embed = {
|
||||||
title: 'Résultats de la recherche',
|
title: "Résultats de la recherche",
|
||||||
description: `🔎 _"${searchValue}"_`,
|
description: `🔎 _"${searchValue}"_`,
|
||||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||||
fields: [{
|
fields: [
|
||||||
name: `**${currentSkin.displayName}**`,
|
{
|
||||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
name: `**${currentSkin.displayName}**`,
|
||||||
}],
|
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||||
image: { url: getImageUrl(skinData) },
|
},
|
||||||
footer: { text: `Résultat 1/${resultSkins.length}` },
|
],
|
||||||
};
|
image: { url: getImageUrl(skinData) },
|
||||||
|
footer: { text: `Résultat 1/${resultSkins.length}` },
|
||||||
|
};
|
||||||
|
|
||||||
// --- 6. Send Final Response ---
|
// --- 6. Send Final Response ---
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
components: components,
|
components: components,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error handling /search command:", error);
|
||||||
console.error('Error handling /search command:', error);
|
return res.status(500).json({ error: "Failed to execute search." });
|
||||||
return res.status(500).json({ error: 'Failed to execute search.' });
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { InteractionResponseType } from 'discord-interactions';
|
import { InteractionResponseType } from "discord-interactions";
|
||||||
import { getTopSkins } from '../../database/index.js';
|
import { getTopSkins } from "../../database/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /skins slash command.
|
* Handles the /skins slash command.
|
||||||
@@ -9,60 +9,59 @@ import { getTopSkins } from '../../database/index.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleSkinsCommand(req, res, client) {
|
export async function handleSkinsCommand(req, res, client) {
|
||||||
const { guild_id } = req.body;
|
const { guild_id } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- 1. Fetch Data ---
|
// --- 1. Fetch Data ---
|
||||||
const topSkins = getTopSkins.all();
|
const topSkins = getTopSkins.all();
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const guild = await client.guilds.fetch(guild_id);
|
||||||
const fields = [];
|
const fields = [];
|
||||||
|
|
||||||
// --- 2. Build Embed Fields Asynchronously ---
|
// --- 2. Build Embed Fields Asynchronously ---
|
||||||
// We use a for...of loop to handle the async fetch for each owner.
|
// We use a for...of loop to handle the async fetch for each owner.
|
||||||
for (const [index, skin] of topSkins.entries()) {
|
for (const [index, skin] of topSkins.entries()) {
|
||||||
let ownerText = 'Libre'; // Default text if the skin has no owner
|
let ownerText = "Libre"; // Default text if the skin has no owner
|
||||||
|
|
||||||
// If the skin has an owner, fetch their details
|
// If the skin has an owner, fetch their details
|
||||||
if (skin.user_id) {
|
if (skin.user_id) {
|
||||||
try {
|
try {
|
||||||
const owner = await guild.members.fetch(skin.user_id);
|
const owner = await guild.members.fetch(skin.user_id);
|
||||||
// Use globalName if available, otherwise fallback to username
|
// Use globalName if available, otherwise fallback to username
|
||||||
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
|
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This can happen if the user has left the server
|
// This can happen if the user has left the server
|
||||||
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`);
|
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`);
|
||||||
ownerText = 'Appartient à un utilisateur inconnu';
|
ownerText = "Appartient à un utilisateur inconnu";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the formatted skin info to our fields array
|
// Add the formatted skin info to our fields array
|
||||||
fields.push({
|
fields.push({
|
||||||
name: `#${index + 1} - **${skin.displayName}**`,
|
name: `#${index + 1} - **${skin.displayName}**`,
|
||||||
value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`,
|
value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`,
|
||||||
inline: false,
|
inline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Send the Response ---
|
// --- 3. Send the Response ---
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: '🏆 Top 10 des Skins les Plus Chers',
|
title: "🏆 Top 10 des Skins les Plus Chers",
|
||||||
description: 'Classement des skins par leur valeur maximale potentielle.',
|
description: "Classement des skins par leur valeur maximale potentielle.",
|
||||||
fields: fields,
|
fields: fields,
|
||||||
color: 0xFFD700, // Gold color for a leaderboard
|
color: 0xffd700, // Gold color for a leaderboard
|
||||||
footer: {
|
footer: {
|
||||||
text: 'Utilisez /inventory pour voir vos propres skins.'
|
text: "Utilisez /inventory pour voir vos propres skins.",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error handling /skins command:", error);
|
||||||
console.error('Error handling /skins command:', error);
|
return res.status(500).json({ error: "Failed to fetch the skins leaderboard." });
|
||||||
return res.status(500).json({ error: 'Failed to fetch the skins leaderboard.' });
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
|
|
||||||
import { formatTime, getOnlineUsersWithRole } from '../../utils/index.js';
|
import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
import { activePolls } from '../../game/state.js';
|
import { activePolls } from "../../game/state.js";
|
||||||
import { getSocketIo } from '../../server/socket.js';
|
import { getSocketIo } from "../../server/socket.js";
|
||||||
import { getUser } from '../../database/index.js';
|
import { getUser } from "../../database/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /timeout slash command.
|
* Handles the /timeout slash command.
|
||||||
@@ -18,193 +18,225 @@ import { getUser } from '../../database/index.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleTimeoutCommand(req, res, client) {
|
export async function handleTimeoutCommand(req, res, client) {
|
||||||
const io = getSocketIo();
|
const io = getSocketIo();
|
||||||
const { id, member, guild_id, channel_id, token, data } = req.body;
|
const { id, member, guild_id, channel_id, token, data } = req.body;
|
||||||
const { options } = data;
|
const { options } = data;
|
||||||
|
|
||||||
// Extract command options
|
// Extract command options
|
||||||
const userId = member.user.id;
|
const userId = member.user.id;
|
||||||
const targetUserId = options[0].value;
|
const targetUserId = options[0].value;
|
||||||
const time = options[1].value;
|
const time = options[1].value;
|
||||||
|
|
||||||
// Fetch member objects from Discord
|
// Fetch member objects from Discord
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const guild = await client.guilds.fetch(guild_id);
|
||||||
const fromMember = await guild.members.fetch(userId);
|
const fromMember = await guild.members.fetch(userId);
|
||||||
const toMember = await guild.members.fetch(targetUserId);
|
const toMember = await guild.members.fetch(targetUserId);
|
||||||
|
|
||||||
// --- Validation Checks ---
|
// --- Validation Checks ---
|
||||||
// 1. Check if a poll is already running for the target user
|
// 1. Check if a poll is already running for the target user
|
||||||
const existingPoll = Object.values(activePolls).find(poll => poll.toUserId === targetUserId);
|
const existingPoll = Object.values(activePolls).find((poll) => poll.toUserId === targetUserId);
|
||||||
if (existingPoll) {
|
if (existingPoll) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`,
|
content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`,
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check if the user is already timed out
|
// 2. Check if the user is already timed out
|
||||||
if (toMember.communicationDisabledUntilTimestamp > Date.now()) {
|
if (toMember.communicationDisabledUntilTimestamp > Date.now()) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: `**${toMember.user.globalName}** est déjà timeout.`,
|
content: `**${toMember.user.globalName}** est déjà timeout.`,
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Poll Initialization ---
|
// --- Poll Initialization ---
|
||||||
const pollId = id; // Use the interaction ID as the unique poll ID
|
const pollId = id; // Use the interaction ID as the unique poll ID
|
||||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
||||||
|
|
||||||
// Calculate required votes
|
// Calculate required votes
|
||||||
const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
|
const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID);
|
||||||
const requiredMajority = Math.max(
|
const requiredMajority = Math.max(
|
||||||
parseInt(process.env.MIN_VOTES, 10),
|
parseInt(process.env.MIN_VOTES, 10),
|
||||||
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1
|
Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Store poll data in the active state
|
// Store poll data in the active state
|
||||||
activePolls[pollId] = {
|
activePolls[pollId] = {
|
||||||
id: userId,
|
id: userId,
|
||||||
username: fromMember.user.globalName,
|
username: fromMember.user.globalName,
|
||||||
toUserId: targetUserId,
|
toUserId: targetUserId,
|
||||||
toUsername: toMember.user.globalName,
|
toUsername: toMember.user.globalName,
|
||||||
time: time,
|
time: time,
|
||||||
time_display: formatTime(time),
|
time_display: formatTime(time),
|
||||||
for: 0,
|
for: 0,
|
||||||
against: 0,
|
against: 0,
|
||||||
voters: [],
|
voters: [],
|
||||||
channelId: channel_id,
|
channelId: channel_id,
|
||||||
endpoint: webhookEndpoint,
|
endpoint: webhookEndpoint,
|
||||||
endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000,
|
endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000,
|
||||||
requiredMajority: requiredMajority,
|
requiredMajority: requiredMajority,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Set up Countdown Interval ---
|
// --- Set up Countdown Interval ---
|
||||||
const countdownInterval = setInterval(async () => {
|
const countdownInterval = setInterval(async () => {
|
||||||
const poll = activePolls[pollId];
|
const poll = activePolls[pollId];
|
||||||
|
|
||||||
// If poll no longer exists, clear the interval
|
// If poll no longer exists, clear the interval
|
||||||
if (!poll) {
|
if (!poll) {
|
||||||
clearInterval(countdownInterval);
|
clearInterval(countdownInterval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||||
|
|
||||||
// --- Poll Expiration Logic ---
|
// --- Poll Expiration Logic ---
|
||||||
if (remaining === 0) {
|
if (remaining === 0) {
|
||||||
clearInterval(countdownInterval);
|
clearInterval(countdownInterval);
|
||||||
|
|
||||||
const votersList = poll.voters.map(voterId => {
|
const votersList = poll.voters
|
||||||
const user = getUser.get(voterId);
|
.map((voterId) => {
|
||||||
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
|
const user = getUser.get(voterId);
|
||||||
}).join('\n');
|
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await DiscordRequest(poll.endpoint, {
|
await DiscordRequest(poll.endpoint, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
|
{
|
||||||
description: `Il manquait **${votesNeeded}** vote(s).`,
|
title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`,
|
||||||
fields: [{
|
description: `Il manquait **${votesNeeded}** vote(s).`,
|
||||||
name: 'Pour',
|
fields: [
|
||||||
value: `✅ ${poll.for}\n${votersList}`,
|
{
|
||||||
inline: true,
|
name: "Pour",
|
||||||
}],
|
value: `✅ ${poll.for}\n${votersList}`,
|
||||||
color: 0xFF4444, // Red for failure
|
inline: true,
|
||||||
}],
|
},
|
||||||
components: [], // Remove buttons
|
],
|
||||||
},
|
color: 0xff4444, // Red for failure
|
||||||
});
|
},
|
||||||
} catch (err) {
|
],
|
||||||
console.error('Error updating failed poll message:', err);
|
components: [], // Remove buttons
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error updating failed poll message:", err);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up the poll from active state
|
// Clean up the poll from active state
|
||||||
delete activePolls[pollId];
|
delete activePolls[pollId];
|
||||||
io.emit('poll-update'); // Notify frontend
|
io.emit("poll-update"); // Notify frontend
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Periodic Update Logic ---
|
// --- Periodic Update Logic ---
|
||||||
// Update the message every second with the new countdown
|
// Update the message every second with the new countdown
|
||||||
try {
|
try {
|
||||||
const votersList = poll.voters.map(voterId => {
|
const votersList = poll.voters
|
||||||
const user = getUser.get(voterId);
|
.map((voterId) => {
|
||||||
return `- ${user?.globalName || 'Utilisateur Inconnu'}`;
|
const user = getUser.get(voterId);
|
||||||
}).join('\n');
|
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
await DiscordRequest(poll.endpoint, {
|
await DiscordRequest(poll.endpoint, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: 'Vote de Timeout',
|
{
|
||||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
title: "Vote de Timeout",
|
||||||
fields: [{
|
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||||
name: 'Pour',
|
fields: [
|
||||||
value: `✅ ${poll.for}\n${votersList}`,
|
{
|
||||||
inline: true,
|
name: "Pour",
|
||||||
}, {
|
value: `✅ ${poll.for}\n${votersList}`,
|
||||||
name: 'Temps restant',
|
inline: true,
|
||||||
value: `⏳ ${countdownText}`,
|
},
|
||||||
inline: false,
|
{
|
||||||
}],
|
name: "Temps restant",
|
||||||
color: 0x5865F2, // Discord Blurple
|
value: `⏳ ${countdownText}`,
|
||||||
}],
|
inline: false,
|
||||||
// Keep the components so people can still vote
|
},
|
||||||
components: [{
|
],
|
||||||
type: MessageComponentTypes.ACTION_ROW,
|
color: 0x5865f2, // Discord Blurple
|
||||||
components: [
|
},
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
|
],
|
||||||
],
|
// Keep the components so people can still vote
|
||||||
}],
|
components: [
|
||||||
},
|
{
|
||||||
});
|
type: MessageComponentTypes.ACTION_ROW,
|
||||||
} catch (err) {
|
components: [
|
||||||
console.error('Error updating countdown:', err);
|
{
|
||||||
// If the message was deleted, stop trying to update it.
|
type: MessageComponentTypes.BUTTON,
|
||||||
if (err.message.includes('Unknown Message')) {
|
custom_id: `vote_for_${pollId}`,
|
||||||
clearInterval(countdownInterval);
|
label: "Oui ✅",
|
||||||
delete activePolls[pollId];
|
style: ButtonStyleTypes.SUCCESS,
|
||||||
io.emit('poll-update');
|
},
|
||||||
}
|
],
|
||||||
}
|
},
|
||||||
}, 2000); // Update every 2 seconds to avoid rate limits
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} 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 ---
|
// --- Send Initial Response ---
|
||||||
io.emit('poll-update'); // Notify frontend
|
io.emit("poll-update"); // Notify frontend
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: 'Vote de Timeout',
|
{
|
||||||
description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
|
title: "Vote de Timeout",
|
||||||
fields: [{
|
description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`,
|
||||||
name: 'Pour',
|
fields: [
|
||||||
value: '✅ 0',
|
{
|
||||||
inline: true,
|
name: "Pour",
|
||||||
}, {
|
value: "✅ 0",
|
||||||
name: 'Temps restant',
|
inline: true,
|
||||||
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
|
},
|
||||||
inline: false,
|
{
|
||||||
}],
|
name: "Temps restant",
|
||||||
color: 0x5865F2,
|
value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`,
|
||||||
}],
|
inline: false,
|
||||||
components: [{
|
},
|
||||||
type: MessageComponentTypes.ACTION_ROW,
|
],
|
||||||
components: [
|
color: 0x5865f2,
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS },
|
},
|
||||||
],
|
],
|
||||||
}],
|
components: [
|
||||||
},
|
{
|
||||||
});
|
type: MessageComponentTypes.ACTION_ROW,
|
||||||
}
|
components: [
|
||||||
|
{
|
||||||
|
type: MessageComponentTypes.BUTTON,
|
||||||
|
custom_id: `vote_for_${pollId}`,
|
||||||
|
label: "Oui ✅",
|
||||||
|
style: ButtonStyleTypes.SUCCESS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
|
||||||
InteractionResponseType,
|
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||||
InteractionResponseFlags,
|
|
||||||
} from 'discord-interactions';
|
|
||||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
|
||||||
|
|
||||||
import { postAPOBuy } from '../../utils/index.js';
|
import { postAPOBuy } from "../../utils/index.js";
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
import {getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
|
import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
|
||||||
import { skins } from '../../game/state.js';
|
import { skins } from "../../game/state.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the /valorant slash command for opening a "skin case".
|
* Handles the /valorant slash command for opening a "skin case".
|
||||||
@@ -17,198 +14,201 @@ import { skins } from '../../game/state.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleValorantCommand(req, res, client) {
|
export async function handleValorantCommand(req, res, client) {
|
||||||
const { member, token } = req.body;
|
const { member, token } = req.body;
|
||||||
const userId = member.user.id;
|
const userId = member.user.id;
|
||||||
const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500;
|
const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- 1. Verify and process payment ---
|
// --- 1. Verify and process payment ---
|
||||||
|
|
||||||
const commandUser = getUser.get(userId);
|
const commandUser = getUser.get(userId);
|
||||||
if (!commandUser) {
|
if (!commandUser) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (commandUser.coins < valoPrice) {
|
if (commandUser.coins < valoPrice) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: `Pas assez de FlopoCoins (${valoPrice} requis).`,
|
content: `Pas assez de FlopoCoins (${valoPrice} requis).`,
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-${Date.now()}`,
|
id: `${userId}-${Date.now()}`,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
action: 'VALO_CASE_OPEN',
|
action: "VALO_CASE_OPEN",
|
||||||
target_user_id: null,
|
target_user_id: null,
|
||||||
coins_amount: -valoPrice,
|
coins_amount: -valoPrice,
|
||||||
user_new_amount: commandUser.coins - valoPrice,
|
user_new_amount: commandUser.coins - valoPrice,
|
||||||
});
|
});
|
||||||
updateUserCoins.run({
|
updateUserCoins.run({
|
||||||
id: userId,
|
id: userId,
|
||||||
coins: commandUser.coins - valoPrice,
|
coins: commandUser.coins - valoPrice,
|
||||||
})
|
});
|
||||||
|
|
||||||
// --- 2. Send Initial "Opening" Response ---
|
// --- 2. Send Initial "Opening" Response ---
|
||||||
// Acknowledge the interaction immediately with a loading message.
|
// Acknowledge the interaction immediately with a loading message.
|
||||||
const initialEmbed = new EmbedBuilder()
|
const initialEmbed = new EmbedBuilder()
|
||||||
.setTitle('Ouverture de la caisse...')
|
.setTitle("Ouverture de la caisse...")
|
||||||
.setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif')
|
.setImage("https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif")
|
||||||
.setColor('#F2F3F3');
|
.setColor("#F2F3F3");
|
||||||
|
|
||||||
await res.send({
|
await res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: { embeds: [initialEmbed] },
|
data: { embeds: [initialEmbed] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- 3. Run the skin reveal logic after a delay ---
|
||||||
|
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 ---
|
// --- Randomize Level and Chroma ---
|
||||||
setTimeout(async () => {
|
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||||
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
|
let randomChroma = 1;
|
||||||
try {
|
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||||
// --- Skin Selection ---
|
// Ensure chroma is at least 1 and not greater than the number of chromas
|
||||||
const availableSkins = getAllAvailableSkins.all();
|
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||||
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 ---
|
// --- Calculate Price ---
|
||||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
const calculatePrice = () => {
|
||||||
let randomChroma = 1;
|
let result = parseFloat(dbSkin.basePrice);
|
||||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
|
||||||
// Ensure chroma is at least 1 and not greater than the number of chromas
|
result *= 1 + randomChroma / 4;
|
||||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
return parseFloat(result.toFixed(0));
|
||||||
}
|
};
|
||||||
|
const finalPrice = calculatePrice();
|
||||||
|
|
||||||
// --- Calculate Price ---
|
// --- Update Database ---
|
||||||
const calculatePrice = () => {
|
await updateSkin.run({
|
||||||
let result = parseFloat(dbSkin.basePrice);
|
uuid: randomSkinData.uuid,
|
||||||
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
|
user_id: userId,
|
||||||
result *= (1 + (randomChroma / 4));
|
currentLvl: randomLevel,
|
||||||
return parseFloat(result.toFixed(0));
|
currentChroma: randomChroma,
|
||||||
};
|
currentPrice: finalPrice,
|
||||||
const finalPrice = calculatePrice();
|
});
|
||||||
|
|
||||||
// --- Update Database ---
|
// --- Prepare Final Embed and Components ---
|
||||||
await updateSkin.run({
|
const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice);
|
||||||
uuid: randomSkinData.uuid,
|
const components = buildComponents(randomSkinData, randomLevel, randomChroma);
|
||||||
user_id: userId,
|
|
||||||
currentLvl: randomLevel,
|
|
||||||
currentChroma: randomChroma,
|
|
||||||
currentPrice: finalPrice,
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Prepare Final Embed and Components ---
|
// --- Edit the Original Message with the Result ---
|
||||||
const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice);
|
await DiscordRequest(webhookEndpoint, {
|
||||||
const components = buildComponents(randomSkinData, randomLevel, randomChroma);
|
method: "PATCH",
|
||||||
|
body: {
|
||||||
// --- Edit the Original Message with the Result ---
|
embeds: [finalEmbed],
|
||||||
await DiscordRequest(webhookEndpoint, {
|
components: components,
|
||||||
method: 'PATCH',
|
},
|
||||||
body: {
|
});
|
||||||
embeds: [finalEmbed],
|
} catch (revealError) {
|
||||||
components: components,
|
console.error("Error during skin reveal:", revealError);
|
||||||
},
|
// Inform the user that something went wrong
|
||||||
});
|
await DiscordRequest(webhookEndpoint, {
|
||||||
|
method: "PATCH",
|
||||||
} catch (revealError) {
|
body: {
|
||||||
console.error('Error during skin reveal:', revealError);
|
content:
|
||||||
// Inform the user that something went wrong
|
"Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.",
|
||||||
await DiscordRequest(webhookEndpoint, {
|
embeds: [],
|
||||||
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.
|
||||||
}, 5000); // 5-second delay for suspense
|
return res.status(500).json({ error: "Failed to initiate the case opening." });
|
||||||
|
}
|
||||||
} 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 ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
/** Builds the final embed to display the won skin. */
|
/** Builds the final embed to display the won skin. */
|
||||||
function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
|
function buildFinalEmbed(dbSkin, skinData, level, chroma, price) {
|
||||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||||
|
|
||||||
const getChromaName = () => {
|
const getChromaName = () => {
|
||||||
if (chroma > 1) {
|
if (chroma > 1) {
|
||||||
const name = selectedChromaData.displayName?.replace(/[\r\n]+/g, ' ').replace(skinData.displayName, '').trim();
|
const name = selectedChromaData.displayName
|
||||||
const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
?.replace(/[\r\n]+/g, " ")
|
||||||
return match ? match[1].trim() : (name || 'Chroma Inconnu');
|
.replace(skinData.displayName, "")
|
||||||
}
|
.trim();
|
||||||
return 'Base';
|
const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||||
};
|
return match ? match[1].trim() : name || "Chroma Inconnu";
|
||||||
|
}
|
||||||
|
return "Base";
|
||||||
|
};
|
||||||
|
|
||||||
const getImageUrl = () => {
|
const getImageUrl = () => {
|
||||||
if (level === skinData.levels.length) {
|
if (level === skinData.levels.length) {
|
||||||
return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon;
|
return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon;
|
||||||
}
|
}
|
||||||
const levelData = skinData.levels[level - 1];
|
const levelData = skinData.levels[level - 1];
|
||||||
return levelData?.displayIcon || skinData.displayIcon;
|
return levelData?.displayIcon || skinData.displayIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lvlText = (level >= 1 ? '1️⃣' : '') +
|
const lvlText =
|
||||||
(level >= 2 ? '2️⃣' : '') +
|
(level >= 1 ? "1️⃣" : "") +
|
||||||
(level >= 3 ? '3️⃣' : '') +
|
(level >= 2 ? "2️⃣" : "") +
|
||||||
(level >= 4 ? '4️⃣' : '') +
|
(level >= 3 ? "3️⃣" : "") +
|
||||||
(level >= 5 ? '5️⃣' : '') +
|
(level >= 4 ? "4️⃣" : "") +
|
||||||
(level >= 6 ? '6️⃣' : '') +
|
(level >= 5 ? "5️⃣" : "") +
|
||||||
'◾'.repeat(skinData.levels.length - level);
|
(level >= 6 ? "6️⃣" : "") +
|
||||||
const chromaText = '💠'.repeat(chroma) + '◾'.repeat(skinData.chromas.length - chroma);
|
"◾".repeat(skinData.levels.length - level);
|
||||||
|
const chromaText = "💠".repeat(chroma) + "◾".repeat(skinData.chromas.length - chroma);
|
||||||
|
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setTitle(`${skinData.displayName} | ${getChromaName()}`)
|
.setTitle(`${skinData.displayName} | ${getChromaName()}`)
|
||||||
.setDescription(dbSkin.tierText)
|
.setDescription(dbSkin.tierText)
|
||||||
.setColor(`#${dbSkin.tierColor}`)
|
.setColor(`#${dbSkin.tierColor}`)
|
||||||
.setImage(getImageUrl())
|
.setImage(getImageUrl())
|
||||||
.setFields([
|
.setFields([
|
||||||
{ name: 'Lvl', value: lvlText || 'N/A', inline: true },
|
{ name: "Lvl", value: lvlText || "N/A", inline: true },
|
||||||
{ name: 'Chroma', value: chromaText || 'N/A', inline: true },
|
{ name: "Chroma", value: chromaText || "N/A", inline: true },
|
||||||
{ name: 'Prix', value: `**${price}** <:vp:1362964205808128122>`, inline: true },
|
{
|
||||||
])
|
name: "Prix",
|
||||||
.setFooter({ text: 'Skin ajouté à votre inventaire !' });
|
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. */
|
/** Builds the action row with a video button if a video is available. */
|
||||||
function buildComponents(skinData, level, chroma) {
|
function buildComponents(skinData, level, chroma) {
|
||||||
const selectedLevelData = skinData.levels[level - 1] || {};
|
const selectedLevelData = skinData.levels[level - 1] || {};
|
||||||
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
const selectedChromaData = skinData.chromas[chroma - 1] || {};
|
||||||
|
|
||||||
let videoUrl = null;
|
let videoUrl = null;
|
||||||
if (level === skinData.levels.length) {
|
if (level === skinData.levels.length) {
|
||||||
videoUrl = selectedChromaData.streamedVideo;
|
videoUrl = selectedChromaData.streamedVideo;
|
||||||
}
|
}
|
||||||
videoUrl = videoUrl || selectedLevelData.streamedVideo;
|
videoUrl = videoUrl || selectedLevelData.streamedVideo;
|
||||||
|
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
return [
|
return [
|
||||||
new ActionRowBuilder().addComponents(
|
new ActionRowBuilder().addComponents(
|
||||||
new ButtonBuilder()
|
new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl),
|
||||||
.setLabel('🎬 Aperçu Vidéo')
|
),
|
||||||
.setStyle(ButtonStyle.Link)
|
];
|
||||||
.setURL(videoUrl)
|
}
|
||||||
)
|
return []; // Return an empty array if no video is available
|
||||||
];
|
}
|
||||||
}
|
|
||||||
return []; // Return an empty array if no video is available
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
|
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
import { activeInventories, skins } from '../../game/state.js';
|
import { activeInventories, skins } from "../../game/state.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles navigation button clicks (Previous/Next) for the inventory embed.
|
* Handles navigation button clicks (Previous/Next) for the inventory embed.
|
||||||
@@ -15,144 +15,167 @@ import { activeInventories, skins } from '../../game/state.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleInventoryNav(req, res, client) {
|
export async function handleInventoryNav(req, res, client) {
|
||||||
const { member, data, guild_id } = req.body;
|
const { member, data, guild_id } = req.body;
|
||||||
const { custom_id } = data;
|
const { custom_id } = data;
|
||||||
|
|
||||||
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id
|
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id
|
||||||
const [direction, page, interactionId] = custom_id.split('_');
|
const [direction, page, interactionId] = custom_id.split("_");
|
||||||
|
|
||||||
// --- 1. Retrieve the interactive session ---
|
// --- 1. Retrieve the interactive session ---
|
||||||
const inventorySession = activeInventories[interactionId];
|
const inventorySession = activeInventories[interactionId];
|
||||||
|
|
||||||
// --- 2. Validation Checks ---
|
// --- 2. Validation Checks ---
|
||||||
if (!inventorySession) {
|
if (!inventorySession) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.",
|
content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the user clicking the button is the one who initiated the command
|
// Ensure the user clicking the button is the one who initiated the command
|
||||||
if (inventorySession.userId !== member.user.id) {
|
if (inventorySession.userId !== member.user.id) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.",
|
content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
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 ---
|
try {
|
||||||
const { amount } = inventorySession;
|
// --- 4. Rebuild Embed with New Page Content ---
|
||||||
if (direction === 'next') {
|
const { page, inventorySkins } = inventorySession;
|
||||||
inventorySession.page = (inventorySession.page + 1) % amount;
|
const currentSkin = inventorySkins[page];
|
||||||
} else if (direction === 'prev') {
|
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||||
inventorySession.page = (inventorySession.page - 1 + amount) % amount;
|
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 {
|
// --- Helper functions for formatting ---
|
||||||
// --- 4. Rebuild Embed with New Page Content ---
|
const getChromaText = (skin, skinInfo) => {
|
||||||
const { page, inventorySkins } = inventorySession;
|
let result = "";
|
||||||
const currentSkin = inventorySkins[page];
|
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||||
if (!skinData) {
|
}
|
||||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
return result || "N/A";
|
||||||
}
|
};
|
||||||
|
|
||||||
const guild = await client.guilds.fetch(guild_id);
|
const getChromaName = (skin, skinInfo) => {
|
||||||
const targetMember = await guild.members.fetch(inventorySession.akhyId);
|
if (skin.currentChroma > 1) {
|
||||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
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 getImageUrl = (skin, skinInfo) => {
|
||||||
const getChromaText = (skin, skinInfo) => {
|
if (skin.currentLvl === skinInfo.levels.length) {
|
||||||
let result = "";
|
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||||
result += skin.currentChroma === i ? '💠 ' : '◾ ';
|
}
|
||||||
}
|
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||||
return result || 'N/A';
|
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getChromaName = (skin, skinInfo) => {
|
// --- 5. Rebuild Components (Buttons) ---
|
||||||
if (skin.currentChroma > 1) {
|
let components = [
|
||||||
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);
|
type: MessageComponentTypes.BUTTON,
|
||||||
return match ? match[1].trim() : name;
|
custom_id: `prev_page_${interactionId}`,
|
||||||
}
|
label: "⏮️ Préc.",
|
||||||
return 'Base';
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
type: MessageComponentTypes.BUTTON,
|
||||||
|
custom_id: `next_page_${interactionId}`,
|
||||||
|
label: "Suiv. ⏭️",
|
||||||
|
style: ButtonStyleTypes.SECONDARY,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const getImageUrl = (skin, skinInfo) => {
|
const isUpgradable =
|
||||||
if (skin.currentLvl === skinInfo.levels.length) {
|
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
// Conditionally add the upgrade button
|
||||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
|
||||||
}
|
components.push({
|
||||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
type: MessageComponentTypes.BUTTON,
|
||||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
custom_id: `upgrade_${interactionId}`,
|
||||||
};
|
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||||
|
style: ButtonStyleTypes.PRIMARY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --- 5. Rebuild Components (Buttons) ---
|
// --- 6. Send PATCH Request to Update the Message ---
|
||||||
let components = [
|
await DiscordRequest(inventorySession.endpoint, {
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY },
|
method: "PATCH",
|
||||||
{ type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY },
|
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;
|
// --- 7. Acknowledge the Interaction ---
|
||||||
// Conditionally add the upgrade button
|
// This tells Discord the interaction was received, and since the message is already updated,
|
||||||
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
|
// no further action is needed.
|
||||||
components.push({
|
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||||
type: MessageComponentTypes.BUTTON,
|
} catch (error) {
|
||||||
custom_id: `upgrade_${interactionId}`,
|
console.error("Error handling inventory navigation:", error);
|
||||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`,
|
// In case of an error, we should still acknowledge the interaction to prevent it from failing.
|
||||||
style: ButtonStyleTypes.PRIMARY,
|
// We can send a silent, ephemeral error message.
|
||||||
});
|
return res.send({
|
||||||
}
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
|
data: {
|
||||||
// --- 6. Send PATCH Request to Update the Message ---
|
content: "Une erreur est survenue lors de la mise à jour de l'inventaire.",
|
||||||
await DiscordRequest(inventorySession.endpoint, {
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import {
|
import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions";
|
||||||
InteractionResponseType,
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
InteractionResponseFlags,
|
import { activePolls } from "../../game/state.js";
|
||||||
} from 'discord-interactions';
|
import { getSocketIo } from "../../server/socket.js";
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { getUser } from "../../database/index.js";
|
||||||
import { activePolls } from '../../game/state.js';
|
|
||||||
import { getSocketIo } from '../../server/socket.js';
|
|
||||||
import { getUser } from '../../database/index.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles clicks on the 'Yes' or 'No' buttons of a timeout poll.
|
* Handles clicks on the 'Yes' or 'No' buttons of a timeout poll.
|
||||||
@@ -13,164 +10,175 @@ import { getUser } from '../../database/index.js';
|
|||||||
* @param {object} res - The Express response object.
|
* @param {object} res - The Express response object.
|
||||||
*/
|
*/
|
||||||
export async function handlePollVote(req, res) {
|
export async function handlePollVote(req, res) {
|
||||||
const io = getSocketIo();
|
const io = getSocketIo();
|
||||||
const { member, data, guild_id } = req.body;
|
const { member, data, guild_id } = req.body;
|
||||||
const { custom_id } = data;
|
const { custom_id } = data;
|
||||||
|
|
||||||
// --- 1. Parse Component ID ---
|
// --- 1. Parse Component ID ---
|
||||||
const [_, voteType, pollId] = custom_id.split('_'); // e.g., ['vote', 'for', '12345...']
|
const [_, voteType, pollId] = custom_id.split("_"); // e.g., ['vote', 'for', '12345...']
|
||||||
const isVotingFor = voteType === 'for';
|
const isVotingFor = voteType === "for";
|
||||||
|
|
||||||
// --- 2. Retrieve Poll and Validate ---
|
// --- 2. Retrieve Poll and Validate ---
|
||||||
const poll = activePolls[pollId];
|
const poll = activePolls[pollId];
|
||||||
const voterId = member.user.id;
|
const voterId = member.user.id;
|
||||||
|
|
||||||
if (!poll) {
|
if (!poll) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Ce sondage de timeout n'est plus actif.",
|
content: "Ce sondage de timeout n'est plus actif.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the voter has the required role
|
// Check if the voter has the required role
|
||||||
if (!member.roles.includes(process.env.VOTING_ROLE_ID)) {
|
if (!member.roles.includes(process.env.VOTING_ROLE_ID)) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Vous n'avez pas le rôle requis pour participer à ce vote.",
|
content: "Vous n'avez pas le rôle requis pour participer à ce vote.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent user from voting on themselves
|
// Prevent user from voting on themselves
|
||||||
if (poll.toUserId === voterId) {
|
if (poll.toUserId === voterId) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Vous ne pouvez pas voter pour vous-même.",
|
content: "Vous ne pouvez pas voter pour vous-même.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent double voting
|
// Prevent double voting
|
||||||
if (poll.voters.includes(voterId)) {
|
if (poll.voters.includes(voterId)) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: 'Vous avez déjà voté pour ce sondage.',
|
content: "Vous avez déjà voté pour ce sondage.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Record the Vote ---
|
// --- 3. Record the Vote ---
|
||||||
poll.voters.push(voterId);
|
poll.voters.push(voterId);
|
||||||
if (isVotingFor) {
|
if (isVotingFor) {
|
||||||
poll.for++;
|
poll.for++;
|
||||||
} else {
|
} else {
|
||||||
poll.against++;
|
poll.against++;
|
||||||
}
|
}
|
||||||
|
|
||||||
io.emit('poll-update'); // Notify frontend clients of the change
|
io.emit("poll-update"); // Notify frontend clients of the change
|
||||||
|
|
||||||
const votersList = poll.voters.map(vId => `- ${getUser.get(vId)?.globalName || 'Utilisateur Inconnu'}`).join('\n');
|
const votersList = poll.voters.map((vId) => `- ${getUser.get(vId)?.globalName || "Utilisateur Inconnu"}`).join("\n");
|
||||||
|
|
||||||
|
// --- 4. Check for Majority ---
|
||||||
|
if (isVotingFor && poll.for >= poll.requiredMajority) {
|
||||||
|
// --- SUCCESS CASE: MAJORITY REACHED ---
|
||||||
|
|
||||||
// --- 4. Check for Majority ---
|
// a. Update the poll message to show success
|
||||||
if (isVotingFor && poll.for >= poll.requiredMajority) {
|
try {
|
||||||
// --- SUCCESS CASE: MAJORITY REACHED ---
|
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
|
// b. Execute the timeout via Discord API
|
||||||
try {
|
try {
|
||||||
await DiscordRequest(poll.endpoint, {
|
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
|
||||||
method: 'PATCH',
|
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
|
||||||
body: {
|
await DiscordRequest(endpointTimeout, {
|
||||||
embeds: [{
|
method: "PATCH",
|
||||||
title: 'Vote Terminé - Timeout Appliqué !',
|
body: { communication_disabled_until: timeoutUntil },
|
||||||
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
|
// c. Send a public confirmation message and clean up
|
||||||
try {
|
delete activePolls[pollId];
|
||||||
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
|
io.emit("poll-update");
|
||||||
const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`;
|
return res.send({
|
||||||
await DiscordRequest(endpointTimeout, {
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
method: 'PATCH',
|
data: {
|
||||||
body: { communication_disabled_until: timeoutUntil },
|
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
|
// a. Send an ephemeral acknowledgment to the voter
|
||||||
delete activePolls[pollId];
|
res.send({
|
||||||
io.emit('poll-update');
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
return res.send({
|
data: {
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
content: "Votre vote a été enregistré ! ✅",
|
||||||
data: {
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
// b. Update the original poll message asynchronously (no need to await)
|
||||||
console.error('Error timing out user:', err);
|
// The main countdown interval will also handle this, but this provides a faster update.
|
||||||
delete activePolls[pollId];
|
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
||||||
io.emit('poll-update');
|
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
||||||
return res.send({
|
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
||||||
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 ---
|
|
||||||
|
|
||||||
// a. Send an ephemeral acknowledgment to the voter
|
DiscordRequest(poll.endpoint, {
|
||||||
res.send({
|
method: "PATCH",
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
body: {
|
||||||
data: {
|
embeds: [
|
||||||
content: 'Votre vote a été enregistré ! ✅',
|
{
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
title: "Vote de Timeout",
|
||||||
},
|
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
||||||
});
|
fields: [
|
||||||
|
{
|
||||||
// b. Update the original poll message asynchronously (no need to await)
|
name: "Pour",
|
||||||
// The main countdown interval will also handle this, but this provides a faster update.
|
value: `✅ ${poll.for}\n${votersList}`,
|
||||||
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
|
inline: true,
|
||||||
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
|
},
|
||||||
const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`;
|
{
|
||||||
|
name: "Temps restant",
|
||||||
DiscordRequest(poll.endpoint, {
|
value: `⏳ ${countdownText}`,
|
||||||
method: 'PATCH',
|
inline: false,
|
||||||
body: {
|
},
|
||||||
embeds: [{
|
],
|
||||||
title: 'Vote de Timeout',
|
color: 0x5865f2,
|
||||||
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`,
|
},
|
||||||
fields: [{
|
],
|
||||||
name: 'Pour',
|
// Keep the original components so people can still vote
|
||||||
value: `✅ ${poll.for}\n${votersList}`,
|
components: req.body.message.components,
|
||||||
inline: true,
|
},
|
||||||
}, {
|
}).catch((err) => console.error("Error updating poll after vote:", err));
|
||||||
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 {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
|
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
import { activeSearchs, skins } from '../../game/state.js';
|
import { activeSearchs, skins } from "../../game/state.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles navigation button clicks (Previous/Next) for the search results embed.
|
* Handles navigation button clicks (Previous/Next) for the search results embed.
|
||||||
@@ -15,107 +15,110 @@ import { activeSearchs, skins } from '../../game/state.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleSearchNav(req, res, client) {
|
export async function handleSearchNav(req, res, client) {
|
||||||
const { member, data, guild_id } = req.body;
|
const { member, data, guild_id } = req.body;
|
||||||
const { custom_id } = data;
|
const { custom_id } = data;
|
||||||
|
|
||||||
// Extract direction and the original interaction ID from the custom_id
|
// Extract direction and the original interaction ID from the custom_id
|
||||||
const [direction, _, page, interactionId] = custom_id.split('_'); // e.g., ['next', 'search', 'page', '123...']
|
const [direction, _, page, interactionId] = custom_id.split("_"); // e.g., ['next', 'search', 'page', '123...']
|
||||||
|
|
||||||
// --- 1. Retrieve the interactive session ---
|
// --- 1. Retrieve the interactive session ---
|
||||||
const searchSession = activeSearchs[interactionId];
|
const searchSession = activeSearchs[interactionId];
|
||||||
|
|
||||||
// --- 2. Validation Checks ---
|
// --- 2. Validation Checks ---
|
||||||
if (!searchSession) {
|
if (!searchSession) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.",
|
content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the user clicking the button is the one who initiated the command
|
// Ensure the user clicking the button is the one who initiated the command
|
||||||
if (searchSession.userId !== member.user.id) {
|
if (searchSession.userId !== member.user.id) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: {
|
data: {
|
||||||
content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.",
|
content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.",
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Update Page Number ---
|
// --- 3. Update Page Number ---
|
||||||
const { amount } = searchSession;
|
const { amount } = searchSession;
|
||||||
if (direction === 'next') {
|
if (direction === "next") {
|
||||||
searchSession.page = (searchSession.page + 1) % amount;
|
searchSession.page = (searchSession.page + 1) % amount;
|
||||||
} else if (direction === 'prev') {
|
} else if (direction === "prev") {
|
||||||
searchSession.page = (searchSession.page - 1 + amount) % amount;
|
searchSession.page = (searchSession.page - 1 + amount) % amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- 4. Rebuild Embed with New Page Content ---
|
// --- 4. Rebuild Embed with New Page Content ---
|
||||||
const { page, resultSkins, searchValue } = searchSession;
|
const { page, resultSkins, searchValue } = searchSession;
|
||||||
const currentSkin = resultSkins[page];
|
const currentSkin = resultSkins[page];
|
||||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||||
if (!skinData) {
|
if (!skinData) {
|
||||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch owner details if the skin is owned
|
// Fetch owner details if the skin is owned
|
||||||
let ownerText = '';
|
let ownerText = "";
|
||||||
if (currentSkin.user_id) {
|
if (currentSkin.user_id) {
|
||||||
try {
|
try {
|
||||||
const owner = await client.users.fetch(currentSkin.user_id);
|
const owner = await client.users.fetch(currentSkin.user_id);
|
||||||
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
|
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`);
|
||||||
ownerText = '| Appartenant à un utilisateur inconnu';
|
ownerText = "| Appartenant à un utilisateur inconnu";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to get the best possible image for the skin
|
// Helper to get the best possible image for the skin
|
||||||
const getImageUrl = (skinInfo) => {
|
const getImageUrl = (skinInfo) => {
|
||||||
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1];
|
||||||
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
if (lastChroma?.fullRender) return lastChroma.fullRender;
|
||||||
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
if (lastChroma?.displayIcon) return lastChroma.displayIcon;
|
||||||
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
const lastLevel = skinInfo.levels[skinInfo.levels.length - 1];
|
||||||
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
if (lastLevel?.displayIcon) return lastLevel.displayIcon;
|
||||||
return skinInfo.displayIcon;
|
return skinInfo.displayIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 5. Send PATCH Request to Update the Message ---
|
// --- 5. Send PATCH Request to Update the Message ---
|
||||||
// Note: The components (buttons) do not change, so we can reuse them from the original message.
|
// Note: The components (buttons) do not change, so we can reuse them from the original message.
|
||||||
await DiscordRequest(searchSession.endpoint, {
|
await DiscordRequest(searchSession.endpoint, {
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
embeds: [{
|
embeds: [
|
||||||
title: 'Résultats de la recherche',
|
{
|
||||||
description: `🔎 _"${searchValue}"_`,
|
title: "Résultats de la recherche",
|
||||||
color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3,
|
description: `🔎 _"${searchValue}"_`,
|
||||||
fields: [{
|
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||||
name: `**${currentSkin.displayName}**`,
|
fields: [
|
||||||
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
{
|
||||||
}],
|
name: `**${currentSkin.displayName}**`,
|
||||||
image: { url: getImageUrl(skinData) },
|
value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`,
|
||||||
footer: { text: `Résultat ${page + 1}/${amount}` },
|
},
|
||||||
}],
|
],
|
||||||
components: req.body.message.components, // Reuse existing components
|
image: { url: getImageUrl(skinData) },
|
||||||
},
|
footer: { text: `Résultat ${page + 1}/${amount}` },
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
components: req.body.message.components, // Reuse existing components
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// --- 6. Acknowledge the Interaction ---
|
// --- 6. Acknowledge the Interaction ---
|
||||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error handling search navigation:", error);
|
||||||
console.error('Error handling search navigation:', error);
|
return res.send({
|
||||||
return res.send({
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
data: {
|
||||||
data: {
|
content: "Une erreur est survenue lors de la mise à jour de la recherche.",
|
||||||
content: 'Une erreur est survenue lors de la mise à jour de la recherche.',
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
},
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
InteractionResponseType,
|
InteractionResponseType,
|
||||||
InteractionResponseFlags,
|
InteractionResponseFlags,
|
||||||
MessageComponentTypes,
|
MessageComponentTypes,
|
||||||
ButtonStyleTypes,
|
ButtonStyleTypes,
|
||||||
} from 'discord-interactions';
|
} from "discord-interactions";
|
||||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
||||||
|
|
||||||
import { DiscordRequest } from '../../api/discord.js';
|
import { DiscordRequest } from "../../api/discord.js";
|
||||||
import { postAPOBuy } from '../../utils/index.js';
|
import { postAPOBuy } from "../../utils/index.js";
|
||||||
import { activeInventories, skins } from '../../game/state.js';
|
import { activeInventories, skins } from "../../game/state.js";
|
||||||
import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js';
|
import { getSkin, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click of the 'Upgrade' button on a skin in the inventory.
|
* Handles the click of the 'Upgrade' button on a skin in the inventory.
|
||||||
@@ -17,202 +17,228 @@ import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../da
|
|||||||
* @param {object} res - The Express response object.
|
* @param {object} res - The Express response object.
|
||||||
*/
|
*/
|
||||||
export async function handleUpgradeSkin(req, res) {
|
export async function handleUpgradeSkin(req, res) {
|
||||||
const { member, data } = req.body;
|
const { member, data } = req.body;
|
||||||
const { custom_id } = data;
|
const { custom_id } = data;
|
||||||
|
|
||||||
const interactionId = custom_id.replace('upgrade_', '');
|
const interactionId = custom_id.replace("upgrade_", "");
|
||||||
const userId = member.user.id;
|
const userId = member.user.id;
|
||||||
|
|
||||||
// --- 1. Retrieve Session and Validate ---
|
// --- 1. Retrieve Session and Validate ---
|
||||||
const inventorySession = activeInventories[interactionId];
|
const inventorySession = activeInventories[interactionId];
|
||||||
if (!inventorySession) {
|
if (!inventorySession) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: { content: "Cet affichage d'inventaire a expiré.", flags: InteractionResponseFlags.EPHEMERAL },
|
data: {
|
||||||
});
|
content: "Cet affichage d'inventaire a expiré.",
|
||||||
}
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the user clicking is the inventory owner
|
// Ensure the user clicking is the inventory owner
|
||||||
if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) {
|
if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) {
|
||||||
return res.send({
|
return res.send({
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
data: { content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.", flags: InteractionResponseFlags.EPHEMERAL },
|
data: {
|
||||||
});
|
content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.",
|
||||||
}
|
flags: InteractionResponseFlags.EPHEMERAL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page];
|
const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page];
|
||||||
const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid);
|
const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid);
|
||||||
|
|
||||||
if (!skinData || (skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)) {
|
if (
|
||||||
return res.send({
|
!skinData ||
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
(skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)
|
||||||
data: { content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.", flags: InteractionResponseFlags.EPHEMERAL },
|
) {
|
||||||
});
|
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 commandUser = getUser.get(userId);
|
||||||
const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10;
|
|
||||||
|
|
||||||
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) {
|
insertLog.run({
|
||||||
return res.send({
|
id: `${userId}-${Date.now()}`,
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
user_id: userId,
|
||||||
data: {
|
action: "VALO_SKIN_UPGRADE",
|
||||||
content: "Erreur lors de la récupération de votre profil utilisateur.",
|
target_user_id: null,
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
coins_amount: -upgradePrice.toFixed(0),
|
||||||
},
|
user_new_amount: commandUser.coins - upgradePrice.toFixed(0),
|
||||||
});
|
});
|
||||||
}
|
updateUserCoins.run({
|
||||||
if (commandUser.coins < upgradePrice) {
|
id: userId,
|
||||||
return res.send({
|
coins: commandUser.coins - upgradePrice.toFixed(0),
|
||||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
});
|
||||||
data: {
|
|
||||||
content: `Pas assez de FlopoCoins (${upgradePrice.toFixed(0)} requis).`,
|
|
||||||
flags: InteractionResponseFlags.EPHEMERAL,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
insertLog.run({
|
// --- 3. Show Loading Animation ---
|
||||||
id: `${userId}-${Date.now()}`,
|
// Acknowledge the click immediately and then edit the message to show a loading state.
|
||||||
user_id: userId,
|
await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||||
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),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
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 ---
|
// --- 4. Perform Upgrade Logic ---
|
||||||
// Acknowledge the click immediately and then edit the message to show a loading state.
|
let succeeded = false;
|
||||||
await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length;
|
||||||
|
|
||||||
await DiscordRequest(inventorySession.endpoint, {
|
if (isLevelUpgrade) {
|
||||||
method: 'PATCH',
|
// Upgrading Level
|
||||||
body: {
|
const successProb =
|
||||||
embeds: [{
|
1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
||||||
title: 'Amélioration en cours...',
|
if (Math.random() < successProb) {
|
||||||
image: { url: 'https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif' },
|
succeeded = true;
|
||||||
color: 0x4F545C,
|
skinToUpgrade.currentLvl++;
|
||||||
}],
|
}
|
||||||
components: [],
|
} 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 ---
|
await updateSkin.run({
|
||||||
let succeeded = false;
|
uuid: skinToUpgrade.uuid,
|
||||||
const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length;
|
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) {
|
// --- 6. Send Final Result ---
|
||||||
// Upgrading Level
|
setTimeout(async () => {
|
||||||
const successProb = 1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5);
|
// Fetch the latest state of the skin from the database
|
||||||
if (Math.random() < successProb) {
|
const finalSkinState = getSkin.get(skinToUpgrade.uuid);
|
||||||
succeeded = true;
|
const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData);
|
||||||
skinToUpgrade.currentLvl++;
|
const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);
|
||||||
}
|
|
||||||
} 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 ---
|
await DiscordRequest(inventorySession.endpoint, {
|
||||||
if (succeeded) {
|
method: "PATCH",
|
||||||
const calculatePrice = () => {
|
body: {
|
||||||
let result = parseFloat(skinToUpgrade.basePrice);
|
embeds: [finalEmbed],
|
||||||
result *= (1 + (skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2)));
|
components: finalComponents,
|
||||||
result *= (1 + (skinToUpgrade.currentChroma / 4));
|
},
|
||||||
return parseFloat(result.toFixed(0));
|
});
|
||||||
};
|
}, 2000); // Delay for the result to feel more impactful
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
/** Builds the result embed (success or failure). */
|
/** Builds the result embed (success or failure). */
|
||||||
function buildFinalEmbed(succeeded, skin, skinData) {
|
function buildFinalEmbed(succeeded, skin, skinData) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(succeeded ? 'Amélioration Réussie ! 🎉' : "L'amélioration a échoué... ❌")
|
.setTitle(succeeded ? "Amélioration Réussie ! 🎉" : "L'amélioration a échoué... ❌")
|
||||||
.setDescription(`**${skin.displayName}**`)
|
.setDescription(`**${skin.displayName}**`)
|
||||||
.setImage(skin.displayIcon) // A static image is fine here
|
.setImage(skin.displayIcon) // A static image is fine here
|
||||||
.setColor(succeeded ? 0x22A55B : 0xED4245);
|
.setColor(succeeded ? 0x22a55b : 0xed4245);
|
||||||
|
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
embed.addFields(
|
embed.addFields(
|
||||||
{ name: 'Nouveau Niveau', value: `${skin.currentLvl}/${skinData.levels.length}`, inline: true },
|
{
|
||||||
{ name: 'Nouveau Chroma', value: `${skin.currentChroma}/${skinData.chromas.length}`, inline: true },
|
name: "Nouveau Niveau",
|
||||||
{ name: 'Nouvelle Valeur', value: `**${skin.currentPrice} Flopos**`, inline: true }
|
value: `${skin.currentLvl}/${skinData.levels.length}`,
|
||||||
);
|
inline: true,
|
||||||
} else {
|
},
|
||||||
embed.addFields({ name: 'Statut', value: 'Aucun changement.' });
|
{
|
||||||
}
|
name: "Nouveau Chroma",
|
||||||
return embed;
|
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). */
|
/** Builds the result components (Retry button or Video link). */
|
||||||
function buildFinalComponents(succeeded, skinData, skin, interactionId) {
|
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();
|
const row = new ActionRowBuilder();
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
// Check for video on the new level/chroma
|
// Check for video on the new level/chroma
|
||||||
const levelData = skinData.levels[skin.currentLvl - 1] || {};
|
const levelData = skinData.levels[skin.currentLvl - 1] || {};
|
||||||
const chromaData = skinData.chromas[skin.currentChroma - 1] || {};
|
const chromaData = skinData.chromas[skin.currentChroma - 1] || {};
|
||||||
const videoUrl = levelData.streamedVideo || chromaData.streamedVideo;
|
const videoUrl = levelData.streamedVideo || chromaData.streamedVideo;
|
||||||
|
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
row.addComponents(new ButtonBuilder().setLabel('🎬 Aperçu Vidéo').setStyle(ButtonStyle.Link).setURL(videoUrl));
|
row.addComponents(new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl));
|
||||||
} else {
|
} else {
|
||||||
return []; // No button if no video
|
return []; // No button if no video
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add a "Retry" button
|
// Add a "Retry" button
|
||||||
row.addComponents(
|
row.addComponents(
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setLabel('Réessayer 🔄️')
|
.setLabel("Réessayer 🔄️")
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
.setCustomId(`upgrade_${interactionId}`)
|
.setCustomId(`upgrade_${interactionId}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return [row];
|
return [row];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { handleMessageCreate } from './handlers/messageCreate.js';
|
import { handleMessageCreate } from "./handlers/messageCreate.js";
|
||||||
import { getAkhys, setupCronJobs } from '../utils/index.js';
|
import { getAkhys, setupCronJobs } from "../utils/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes and attaches all necessary event listeners to the Discord client.
|
* Initializes and attaches all necessary event listeners to the Discord client.
|
||||||
@@ -9,44 +9,44 @@ import { getAkhys, setupCronJobs } from '../utils/index.js';
|
|||||||
* @param {object} io - The Socket.IO server instance for real-time communication.
|
* @param {object} io - The Socket.IO server instance for real-time communication.
|
||||||
*/
|
*/
|
||||||
export function initializeEvents(client, io) {
|
export function initializeEvents(client, io) {
|
||||||
// --- on 'ready' ---
|
// --- on 'ready' ---
|
||||||
// This event fires once the bot has successfully logged in and is ready to operate.
|
// This event fires once the bot has successfully logged in and is ready to operate.
|
||||||
// It's a good place for setup tasks that require the bot to be online.
|
// It's a good place for setup tasks that require the bot to be online.
|
||||||
client.once('ready', async () => {
|
client.once("clientReady", async () => {
|
||||||
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
|
console.log(`Bot is ready and logged in as ${client.user.tag}!`);
|
||||||
console.log('[Startup] Bot is ready, performing initial data sync...');
|
console.log("[Startup] Bot is ready, performing initial data sync...");
|
||||||
await getAkhys(client);
|
await getAkhys(client);
|
||||||
console.log('[Startup] Setting up scheduled tasks...');
|
console.log("[Startup] Setting up scheduled tasks...");
|
||||||
setupCronJobs(client, io);
|
setupCronJobs(client, io);
|
||||||
console.log('--- FlopoBOT is fully operational ---');
|
console.log("--- FlopoBOT is fully operational ---");
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- on 'messageCreate' ---
|
// --- on 'messageCreate' ---
|
||||||
// This event fires every time a message is sent in a channel the bot can see.
|
// This event fires every time a message is sent in a channel the bot can see.
|
||||||
// The logic is delegated to its own dedicated handler for cleanliness.
|
// The logic is delegated to its own dedicated handler for cleanliness.
|
||||||
client.on('messageCreate', async (message) => {
|
client.on("messageCreate", async (message) => {
|
||||||
// We pass the client and io instances to the handler so it has access to them
|
// We pass the client and io instances to the handler so it has access to them
|
||||||
// without needing to import them, preventing potential circular dependencies.
|
// without needing to import them, preventing potential circular dependencies.
|
||||||
await handleMessageCreate(message, client, io);
|
await handleMessageCreate(message, client, io);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- on 'interactionCreate' (Alternative Method) ---
|
// --- on 'interactionCreate' (Alternative Method) ---
|
||||||
// While we handle interactions via the Express endpoint for scalability and statelessness,
|
// While we handle interactions via the Express endpoint for scalability and statelessness,
|
||||||
// you could also listen for them via the gateway like this.
|
// you could also listen for them via the gateway like this.
|
||||||
// It's commented out because our current architecture uses the webhook approach.
|
// It's commented out because our current architecture uses the webhook approach.
|
||||||
/*
|
/*
|
||||||
client.on('interactionCreate', async (interaction) => {
|
client.on('interactionCreate', async (interaction) => {
|
||||||
// Logic to handle interactions would go here if not using a webhook endpoint.
|
// 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.
|
// You can add more event listeners here as your bot's functionality grows.
|
||||||
// For example, listening for new members joining the server:
|
// For example, listening for new members joining the server:
|
||||||
// client.on('guildMemberAdd', (member) => {
|
// client.on('guildMemberAdd', (member) => {
|
||||||
// console.log(`Welcome to the server, ${member.user.tag}!`);
|
// console.log(`Welcome to the server, ${member.user.tag}!`);
|
||||||
// const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID');
|
// const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID');
|
||||||
// if (welcomeChannel) {
|
// if (welcomeChannel) {
|
||||||
// welcomeChannel.send(`Please welcome <@${member.id}> to the server!`);
|
// welcomeChannel.send(`Please welcome <@${member.id}> to the server!`);
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import {
|
import { InteractionType, InteractionResponseType } from "discord-interactions";
|
||||||
InteractionType,
|
|
||||||
InteractionResponseType,
|
|
||||||
} from 'discord-interactions';
|
|
||||||
|
|
||||||
// --- Command Handlers ---
|
// --- Command Handlers ---
|
||||||
import { handleTimeoutCommand } from '../commands/timeout.js';
|
import { handleTimeoutCommand } from "../commands/timeout.js";
|
||||||
import { handleInventoryCommand } from '../commands/inventory.js';
|
import { handleInventoryCommand } from "../commands/inventory.js";
|
||||||
import { handleValorantCommand } from '../commands/valorant.js';
|
import { handleValorantCommand } from "../commands/valorant.js";
|
||||||
import { handleInfoCommand } from '../commands/info.js';
|
import { handleInfoCommand } from "../commands/info.js";
|
||||||
import { handleSkinsCommand } from '../commands/skins.js';
|
import { handleSkinsCommand } from "../commands/skins.js";
|
||||||
import { handleSearchCommand } from '../commands/search.js';
|
import { handleSearchCommand } from "../commands/search.js";
|
||||||
import { handleFlopoSiteCommand } from '../commands/floposite.js';
|
import { handleFlopoSiteCommand } from "../commands/floposite.js";
|
||||||
|
|
||||||
// --- Component Handlers ---
|
// --- Component Handlers ---
|
||||||
import { handlePollVote } from '../components/pollVote.js';
|
import { handlePollVote } from "../components/pollVote.js";
|
||||||
import { handleInventoryNav } from '../components/inventoryNav.js';
|
import { handleInventoryNav } from "../components/inventoryNav.js";
|
||||||
import { handleUpgradeSkin } from '../components/upgradeSkin.js';
|
import { handleUpgradeSkin } from "../components/upgradeSkin.js";
|
||||||
import { handleSearchNav } from '../components/searchNav.js';
|
import { handleSearchNav } from "../components/searchNav.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main handler for all incoming interactions from Discord.
|
* The main handler for all incoming interactions from Discord.
|
||||||
@@ -25,65 +22,64 @@ import { handleSearchNav } from '../components/searchNav.js';
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function handleInteraction(req, res, client) {
|
export async function handleInteraction(req, res, client) {
|
||||||
const { type, data, id } = req.body;
|
const { type, data, id } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type === InteractionType.PING) {
|
if (type === InteractionType.PING) {
|
||||||
return res.send({ type: InteractionResponseType.PONG });
|
return res.send({ type: InteractionResponseType.PONG });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === InteractionType.APPLICATION_COMMAND) {
|
if (type === InteractionType.APPLICATION_COMMAND) {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'timeout':
|
case "timeout":
|
||||||
return await handleTimeoutCommand(req, res, client);
|
return await handleTimeoutCommand(req, res, client);
|
||||||
case 'inventory':
|
case "inventory":
|
||||||
return await handleInventoryCommand(req, res, client, id);
|
return await handleInventoryCommand(req, res, client, id);
|
||||||
case 'valorant':
|
case "valorant":
|
||||||
return await handleValorantCommand(req, res, client);
|
return await handleValorantCommand(req, res, client);
|
||||||
case 'info':
|
case "info":
|
||||||
return await handleInfoCommand(req, res, client);
|
return await handleInfoCommand(req, res, client);
|
||||||
case 'skins':
|
case "skins":
|
||||||
return await handleSkinsCommand(req, res, client);
|
return await handleSkinsCommand(req, res, client);
|
||||||
case 'search':
|
case "search":
|
||||||
return await handleSearchCommand(req, res, client, id);
|
return await handleSearchCommand(req, res, client, id);
|
||||||
case 'floposite':
|
case "floposite":
|
||||||
return await handleFlopoSiteCommand(req, res);
|
return await handleFlopoSiteCommand(req, res);
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown command: ${name}`);
|
console.error(`Unknown command: ${name}`);
|
||||||
return res.status(400).json({ error: 'Unknown command' });
|
return res.status(400).json({ error: "Unknown command" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === InteractionType.MESSAGE_COMPONENT) {
|
if (type === InteractionType.MESSAGE_COMPONENT) {
|
||||||
const componentId = data.custom_id;
|
const componentId = data.custom_id;
|
||||||
|
|
||||||
if (componentId.startsWith('vote_')) {
|
if (componentId.startsWith("vote_")) {
|
||||||
return await handlePollVote(req, res, client);
|
return await handlePollVote(req, res, client);
|
||||||
}
|
}
|
||||||
if (componentId.startsWith('prev_page') || componentId.startsWith('next_page')) {
|
if (componentId.startsWith("prev_page") || componentId.startsWith("next_page")) {
|
||||||
return await handleInventoryNav(req, res, client);
|
return await handleInventoryNav(req, res, client);
|
||||||
}
|
}
|
||||||
if (componentId.startsWith('upgrade_')) {
|
if (componentId.startsWith("upgrade_")) {
|
||||||
return await handleUpgradeSkin(req, res, client);
|
return await handleUpgradeSkin(req, res, client);
|
||||||
}
|
}
|
||||||
if (componentId.startsWith('prev_search_page') || componentId.startsWith('next_search_page')) {
|
if (componentId.startsWith("prev_search_page") || componentId.startsWith("next_search_page")) {
|
||||||
return await handleSearchNav(req, res, client);
|
return await handleSearchNav(req, res, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for other potential components
|
// Fallback for other potential components
|
||||||
console.error(`Unknown component ID: ${componentId}`);
|
console.error(`Unknown component ID: ${componentId}`);
|
||||||
return res.status(400).json({ error: 'Unknown component' });
|
return res.status(400).json({ error: "Unknown component" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fallback for Unknown Interaction Types ---
|
// --- Fallback for Unknown Interaction Types ---
|
||||||
console.error('Unknown interaction type:', type);
|
console.error("Unknown interaction type:", type);
|
||||||
return res.status(400).json({ error: 'Unknown interaction type' });
|
return res.status(400).json({ error: "Unknown interaction type" });
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error handling interaction:", error);
|
||||||
console.error('Error handling interaction:', error);
|
// Send a generic error response to Discord if something goes wrong
|
||||||
// Send a generic error response to Discord if something goes wrong
|
return res.status(500).json({ error: "An internal error occurred" });
|
||||||
return res.status(500).json({ error: 'An internal error occurred' });
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,113 +1,121 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import { getTimesChoices } from '../game/various.js';
|
import { getTimesChoices } from "../game/various.js";
|
||||||
import { capitalize, InstallGlobalCommands } from '../utils/index.js';
|
import { capitalize, InstallGlobalCommands } from "../utils/index.js";
|
||||||
|
|
||||||
function createTimesChoices() {
|
function createTimesChoices() {
|
||||||
const choices = getTimesChoices();
|
const choices = getTimesChoices();
|
||||||
const commandChoices = [];
|
const commandChoices = [];
|
||||||
|
|
||||||
for (let choice of choices) {
|
for (let choice of choices) {
|
||||||
commandChoices.push({
|
commandChoices.push({
|
||||||
name: capitalize(choice.name),
|
name: capitalize(choice.name),
|
||||||
value: choice.value?.toString(),
|
value: choice.value?.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return commandChoices;
|
return commandChoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout vote command
|
// Timeout vote command
|
||||||
const TIMEOUT_COMMAND = {
|
const TIMEOUT_COMMAND = {
|
||||||
name: 'timeout',
|
name: "timeout",
|
||||||
description: 'Vote démocratique pour timeout un boug',
|
description: "Vote démocratique pour timeout un boug",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
type: 6,
|
type: 6,
|
||||||
name: 'akhy',
|
name: "akhy",
|
||||||
description: 'Qui ?',
|
description: "Qui ?",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 3,
|
type: 3,
|
||||||
name: 'temps',
|
name: "temps",
|
||||||
description: 'Combien de temps ?',
|
description: "Combien de temps ?",
|
||||||
required: true,
|
required: true,
|
||||||
choices: createTimesChoices(),
|
choices: createTimesChoices(),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
// Valorant
|
// Valorant
|
||||||
const VALORANT_COMMAND = {
|
const VALORANT_COMMAND = {
|
||||||
name: 'valorant',
|
name: "valorant",
|
||||||
description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
|
description: `Ouvrir une caisse valorant (500 FlopoCoins)`,
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
// Own inventory command
|
// Own inventory command
|
||||||
const INVENTORY_COMMAND = {
|
const INVENTORY_COMMAND = {
|
||||||
name: 'inventory',
|
name: "inventory",
|
||||||
description: 'Voir inventaire',
|
description: "Voir inventaire",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
type: 6,
|
type: 6,
|
||||||
name: 'akhy',
|
name: "akhy",
|
||||||
description: 'Qui ?',
|
description: "Qui ?",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
const INFO_COMMAND = {
|
const INFO_COMMAND = {
|
||||||
name: 'info',
|
name: "info",
|
||||||
description: 'Qui est time out ?',
|
description: "Qui est time out ?",
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
const SKINS_COMMAND = {
|
const SKINS_COMMAND = {
|
||||||
name: 'skins',
|
name: "skins",
|
||||||
description: 'Le top 10 des skins les plus chers.',
|
description: "Le top 10 des skins les plus chers.",
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
const SITE_COMMAND = {
|
const SITE_COMMAND = {
|
||||||
name: 'floposite',
|
name: "floposite",
|
||||||
description: 'Lien vers FlopoSite',
|
description: "Lien vers FlopoSite",
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
const SEARCH_SKIN_COMMAND = {
|
const SEARCH_SKIN_COMMAND = {
|
||||||
name: 'search',
|
name: "search",
|
||||||
description: 'Chercher un skin',
|
description: "Chercher un skin",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
type: 3,
|
type: 3,
|
||||||
name: 'recherche',
|
name: "recherche",
|
||||||
description: 'Tu cherches quoi ?',
|
description: "Tu cherches quoi ?",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
type: 1,
|
type: 1,
|
||||||
integration_types: [0, 1],
|
integration_types: [0, 1],
|
||||||
contexts: [0, 2],
|
contexts: [0, 2],
|
||||||
}
|
};
|
||||||
|
|
||||||
const ALL_COMMANDS = [TIMEOUT_COMMAND, INVENTORY_COMMAND, VALORANT_COMMAND, INFO_COMMAND, SKINS_COMMAND, SEARCH_SKIN_COMMAND, SITE_COMMAND];
|
const ALL_COMMANDS = [
|
||||||
|
TIMEOUT_COMMAND,
|
||||||
|
INVENTORY_COMMAND,
|
||||||
|
VALORANT_COMMAND,
|
||||||
|
INFO_COMMAND,
|
||||||
|
SKINS_COMMAND,
|
||||||
|
SEARCH_SKIN_COMMAND,
|
||||||
|
SITE_COMMAND,
|
||||||
|
];
|
||||||
|
|
||||||
export function registerCommands() {
|
export function registerCommands() {
|
||||||
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
|
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +1,381 @@
|
|||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export const flopoDB = new Database("flopobot.db");
|
||||||
export const flopoDB = new Database('flopobot.db');
|
|
||||||
|
|
||||||
export const stmtUsers = flopoDB.prepare(`
|
export const stmtUsers = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users
|
||||||
id TEXT PRIMARY KEY,
|
(
|
||||||
username TEXT NOT NULL,
|
id TEXT PRIMARY KEY,
|
||||||
globalName TEXT,
|
username TEXT NOT NULL,
|
||||||
warned BOOLEAN DEFAULT 0,
|
globalName TEXT,
|
||||||
warns INTEGER DEFAULT 0,
|
warned BOOLEAN DEFAULT 0,
|
||||||
allTimeWarns INTEGER DEFAULT 0,
|
warns INTEGER DEFAULT 0,
|
||||||
totalRequests INTEGER DEFAULT 0,
|
allTimeWarns INTEGER DEFAULT 0,
|
||||||
coins INTEGER DEFAULT 0,
|
totalRequests INTEGER DEFAULT 0,
|
||||||
dailyQueried BOOLEAN DEFAULT 0,
|
coins INTEGER DEFAULT 0,
|
||||||
avatarUrl TEXT DEFAULT NULL,
|
dailyQueried BOOLEAN DEFAULT 0,
|
||||||
isAkhy BOOLEAN DEFAULT 0
|
avatarUrl TEXT DEFAULT NULL,
|
||||||
)
|
isAkhy BOOLEAN DEFAULT 0
|
||||||
|
)
|
||||||
`);
|
`);
|
||||||
stmtUsers.run();
|
stmtUsers.run();
|
||||||
export const stmtSkins = flopoDB.prepare(`
|
export const stmtSkins = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS skins (
|
CREATE TABLE IF NOT EXISTS skins
|
||||||
uuid TEXT PRIMARY KEY,
|
(
|
||||||
displayName TEXT,
|
uuid TEXT PRIMARY KEY,
|
||||||
contentTierUuid TEXT,
|
displayName TEXT,
|
||||||
displayIcon TEXT,
|
contentTierUuid TEXT,
|
||||||
user_id TEXT REFERENCES users,
|
displayIcon TEXT,
|
||||||
tierRank TEXT,
|
user_id TEXT REFERENCES users,
|
||||||
tierColor TEXT,
|
tierRank TEXT,
|
||||||
tierText TEXT,
|
tierColor TEXT,
|
||||||
basePrice TEXT,
|
tierText TEXT,
|
||||||
currentLvl INTEGER DEFAULT NULL,
|
basePrice TEXT,
|
||||||
currentChroma INTEGER DEFAULT NULL,
|
currentLvl INTEGER DEFAULT NULL,
|
||||||
currentPrice INTEGER DEFAULT NULL,
|
currentChroma INTEGER DEFAULT NULL,
|
||||||
maxPrice 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 insertUser = flopoDB.prepare(
|
||||||
export const updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id');
|
`INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
|
||||||
export const updateUserAvatar = flopoDB.prepare('UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id');
|
VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
|
||||||
export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`);
|
);
|
||||||
export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0`);
|
export const updateUser = flopoDB.prepare(
|
||||||
export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id');
|
`UPDATE users
|
||||||
export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?');
|
SET warned = @warned,
|
||||||
export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC');
|
warns = @warns,
|
||||||
export const getAllAkhys = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC');
|
allTimeWarns = @allTimeWarns,
|
||||||
|
totalRequests = @totalRequests
|
||||||
|
WHERE id = @id`,
|
||||||
|
);
|
||||||
|
export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id");
|
||||||
|
export const queryDailyReward = flopoDB.prepare(`UPDATE users
|
||||||
|
SET dailyQueried = 1
|
||||||
|
WHERE id = ?`);
|
||||||
|
export const resetDailyReward = flopoDB.prepare(`UPDATE users
|
||||||
|
SET dailyQueried = 0`);
|
||||||
|
export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id");
|
||||||
|
export const getUser = flopoDB.prepare(
|
||||||
|
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?",
|
||||||
|
);
|
||||||
|
export const getAllUsers = flopoDB.prepare(
|
||||||
|
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC",
|
||||||
|
);
|
||||||
|
export const getAllAkhys = flopoDB.prepare(
|
||||||
|
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC",
|
||||||
|
);
|
||||||
|
|
||||||
export const insertSkin = flopoDB.prepare('INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, basePrice, currentLvl, currentChroma, currentPrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)');
|
export const insertSkin = flopoDB.prepare(
|
||||||
export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid');
|
`INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
|
||||||
export const hardUpdateSkin = flopoDB.prepare('UPDATE skins SET displayName = @displayName, contentTierUuid = @contentTierUuid, displayIcon = @displayIcon, tierRank = @tierRank, tierColor = @tierColor, tierText = @tierText, basePrice = @basePrice, user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice, maxPrice = @maxPrice WHERE uuid = @uuid');
|
basePrice, currentLvl, currentChroma, currentPrice, maxPrice)
|
||||||
export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?');
|
VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
|
||||||
export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC');
|
@basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`,
|
||||||
export const getAllAvailableSkins = flopoDB.prepare('SELECT * FROM skins WHERE user_id IS NULL');
|
);
|
||||||
export const getUserInventory = flopoDB.prepare('SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC');
|
export const updateSkin = flopoDB.prepare(
|
||||||
export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10');
|
`UPDATE skins
|
||||||
|
SET user_id = @user_id,
|
||||||
|
currentLvl = @currentLvl,
|
||||||
|
currentChroma = @currentChroma,
|
||||||
|
currentPrice = @currentPrice
|
||||||
|
WHERE uuid = @uuid`,
|
||||||
|
);
|
||||||
|
export const hardUpdateSkin = flopoDB.prepare(
|
||||||
|
`UPDATE skins
|
||||||
|
SET displayName = @displayName,
|
||||||
|
contentTierUuid = @contentTierUuid,
|
||||||
|
displayIcon = @displayIcon,
|
||||||
|
tierRank = @tierRank,
|
||||||
|
tierColor = @tierColor,
|
||||||
|
tierText = @tierText,
|
||||||
|
basePrice = @basePrice,
|
||||||
|
user_id = @user_id,
|
||||||
|
currentLvl = @currentLvl,
|
||||||
|
currentChroma = @currentChroma,
|
||||||
|
currentPrice = @currentPrice,
|
||||||
|
maxPrice = @maxPrice
|
||||||
|
WHERE uuid = @uuid`,
|
||||||
|
);
|
||||||
|
export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?");
|
||||||
|
export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC");
|
||||||
|
export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL");
|
||||||
|
export const getUserInventory = flopoDB.prepare(
|
||||||
|
"SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC",
|
||||||
|
);
|
||||||
|
export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10");
|
||||||
|
|
||||||
|
export const stmtMarketOffers = flopoDB.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS market_offers
|
||||||
|
(
|
||||||
|
id PRIMARY KEY,
|
||||||
|
skin_uuid TEXT REFERENCES skins,
|
||||||
|
seller_id TEXT REFERENCES users,
|
||||||
|
starting_price INTEGER NOT NULL,
|
||||||
|
buyout_price INTEGER DEFAULT NULL,
|
||||||
|
final_price INTEGER DEFAULT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
opening_at TIMESTAMP NOT NULL,
|
||||||
|
closing_at TIMESTAMP NOT NULL,
|
||||||
|
buyer_id TEXT REFERENCES users DEFAULT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
stmtMarketOffers.run();
|
||||||
|
|
||||||
|
export const stmtBids = flopoDB.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS bids
|
||||||
|
(
|
||||||
|
id PRIMARY KEY,
|
||||||
|
bidder_id TEXT REFERENCES users,
|
||||||
|
market_offer_id REFERENCES market_offers,
|
||||||
|
offer_amount INTEGER,
|
||||||
|
offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
stmtBids.run();
|
||||||
|
|
||||||
|
export const getMarketOffers = flopoDB.prepare(`
|
||||||
|
SELECT market_offers.*,
|
||||||
|
skins.displayName AS skinName,
|
||||||
|
skins.displayIcon AS skinIcon,
|
||||||
|
seller.username AS sellerName,
|
||||||
|
seller.globalName AS sellerGlobalName,
|
||||||
|
buyer.username AS buyerName,
|
||||||
|
buyer.globalName AS buyerGlobalName
|
||||||
|
FROM market_offers
|
||||||
|
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||||
|
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||||
|
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||||
|
ORDER BY market_offers.posted_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const getMarketOfferById = flopoDB.prepare(`
|
||||||
|
SELECT market_offers.*,
|
||||||
|
skins.displayName AS skinName,
|
||||||
|
skins.displayIcon AS skinIcon,
|
||||||
|
seller.username AS sellerName,
|
||||||
|
seller.globalName AS sellerGlobalName,
|
||||||
|
buyer.username AS buyerName,
|
||||||
|
buyer.globalName AS buyerGlobalName
|
||||||
|
FROM market_offers
|
||||||
|
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||||
|
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||||
|
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||||
|
WHERE market_offers.id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const insertMarketOffer = flopoDB.prepare(`
|
||||||
|
INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at)
|
||||||
|
VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at)
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const getBids = flopoDB.prepare(`
|
||||||
|
SELECT bids.*,
|
||||||
|
bidder.username AS bidderName,
|
||||||
|
bidder.globalName AS bidderGlobalName
|
||||||
|
FROM bids
|
||||||
|
JOIN users AS bidder ON bidder.id = bids.bidder_id
|
||||||
|
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const getBidById = flopoDB.prepare(`
|
||||||
|
SELECT bids.*
|
||||||
|
FROM bids
|
||||||
|
WHERE bids.id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const getOfferBids = flopoDB.prepare(`
|
||||||
|
SELECT bids.*
|
||||||
|
FROM bids
|
||||||
|
WHERE bids.market_offer_id = ?
|
||||||
|
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
export const insertBid = flopoDB.prepare(`
|
||||||
|
INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount)
|
||||||
|
VALUES (@id, @bidder_id, @market_offer_id, @offer_amount)
|
||||||
|
`);
|
||||||
|
|
||||||
export const insertManyUsers = flopoDB.transaction(async (users) => {
|
export const insertManyUsers = flopoDB.transaction(async (users) => {
|
||||||
for (const user of users) try { await insertUser.run(user) } catch (e) { /**/ }
|
for (const user of users)
|
||||||
|
try {
|
||||||
|
await insertUser.run(user);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
export const updateManyUsers = flopoDB.transaction(async (users) => {
|
export const updateManyUsers = flopoDB.transaction(async (users) => {
|
||||||
for (const user of users) try { await updateUser.run(user) } catch (e) { console.log('user update failed') }
|
for (const user of users)
|
||||||
|
try {
|
||||||
|
await updateUser.run(user);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("user update failed");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const insertManySkins = flopoDB.transaction(async (skins) => {
|
export const insertManySkins = flopoDB.transaction(async (skins) => {
|
||||||
for (const skin of skins) try { await insertSkin.run(skin) } catch (e) {}
|
for (const skin of skins)
|
||||||
|
try {
|
||||||
|
await insertSkin.run(skin);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
export const updateManySkins = flopoDB.transaction(async (skins) => {
|
export const updateManySkins = flopoDB.transaction(async (skins) => {
|
||||||
for (const skin of skins) try { await updateSkin.run(skin) } catch (e) {}
|
for (const skin of skins)
|
||||||
|
try {
|
||||||
|
await updateSkin.run(skin);
|
||||||
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const stmtLogs = flopoDB.prepare(`
|
export const stmtLogs = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
CREATE TABLE IF NOT EXISTS logs
|
||||||
id PRIMARY KEY,
|
(
|
||||||
user_id TEXT REFERENCES users,
|
id PRIMARY KEY,
|
||||||
action TEXT,
|
user_id TEXT REFERENCES users,
|
||||||
target_user_id TEXT REFERENCES users,
|
action TEXT,
|
||||||
coins_amount INTEGER,
|
target_user_id TEXT REFERENCES users,
|
||||||
user_new_amount INTEGER
|
coins_amount INTEGER,
|
||||||
)
|
user_new_amount INTEGER
|
||||||
|
)
|
||||||
`);
|
`);
|
||||||
stmtLogs.run()
|
stmtLogs.run();
|
||||||
|
|
||||||
export const insertLog = flopoDB.prepare('INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)');
|
|
||||||
export const getLogs = flopoDB.prepare('SELECT * FROM logs');
|
|
||||||
export const getUserLogs = flopoDB.prepare('SELECT * FROM logs WHERE user_id = @user_id');
|
|
||||||
|
|
||||||
|
export const insertLog = flopoDB.prepare(
|
||||||
|
`INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount)
|
||||||
|
VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`,
|
||||||
|
);
|
||||||
|
export const getLogs = flopoDB.prepare("SELECT * FROM logs");
|
||||||
|
export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id");
|
||||||
|
|
||||||
export const stmtGames = flopoDB.prepare(`
|
export const stmtGames = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS games (
|
CREATE TABLE IF NOT EXISTS games
|
||||||
id PRIMARY KEY,
|
(
|
||||||
p1 TEXT REFERENCES users,
|
id PRIMARY KEY,
|
||||||
p2 TEXT REFERENCES users,
|
p1 TEXT REFERENCES users,
|
||||||
p1_score INTEGER,
|
p2 TEXT REFERENCES users,
|
||||||
p2_score INTEGER,
|
p1_score INTEGER,
|
||||||
p1_elo INTEGER,
|
p2_score INTEGER,
|
||||||
p2_elo INTEGER,
|
p1_elo INTEGER,
|
||||||
p1_new_elo INTEGER,
|
p2_elo INTEGER,
|
||||||
p2_new_elo INTEGER,
|
p1_new_elo INTEGER,
|
||||||
type TEXT,
|
p2_new_elo INTEGER,
|
||||||
timestamp TIMESTAMP
|
type TEXT,
|
||||||
)
|
timestamp TIMESTAMP
|
||||||
|
)
|
||||||
`);
|
`);
|
||||||
stmtGames.run()
|
stmtGames.run();
|
||||||
|
|
||||||
export const insertGame = flopoDB.prepare('INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)');
|
|
||||||
export const getGames = flopoDB.prepare('SELECT * FROM games');
|
|
||||||
export const getUserGames = flopoDB.prepare('SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp');
|
|
||||||
|
|
||||||
|
export const insertGame = flopoDB.prepare(
|
||||||
|
`INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp)
|
||||||
|
VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`,
|
||||||
|
);
|
||||||
|
export const getGames = flopoDB.prepare("SELECT * FROM games");
|
||||||
|
export const getUserGames = flopoDB.prepare(
|
||||||
|
"SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp",
|
||||||
|
);
|
||||||
|
|
||||||
export const stmtElos = flopoDB.prepare(`
|
export const stmtElos = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS elos (
|
CREATE TABLE IF NOT EXISTS elos
|
||||||
id PRIMARY KEY REFERENCES users,
|
(
|
||||||
elo INTEGER
|
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 insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
|
||||||
export const getElos = flopoDB.prepare(`SELECT * FROM elos`);
|
VALUES (@id, @elo)`);
|
||||||
export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`);
|
export const getElos = flopoDB.prepare(`SELECT *
|
||||||
export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id');
|
FROM elos`);
|
||||||
|
export const getUserElo = flopoDB.prepare(`SELECT *
|
||||||
|
FROM elos
|
||||||
|
WHERE id = @id`);
|
||||||
|
export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id");
|
||||||
|
|
||||||
|
export const getUsersByElo = flopoDB.prepare(
|
||||||
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC')
|
"SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
|
||||||
|
);
|
||||||
|
|
||||||
export const stmtSOTD = flopoDB.prepare(`
|
export const stmtSOTD = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS sotd (
|
CREATE TABLE IF NOT EXISTS sotd
|
||||||
id INT PRIMARY KEY,
|
(
|
||||||
tableauPiles TEXT,
|
id INT PRIMARY KEY,
|
||||||
foundationPiles TEXT,
|
tableauPiles TEXT,
|
||||||
stockPile TEXT,
|
foundationPiles TEXT,
|
||||||
wastePile TEXT,
|
stockPile TEXT,
|
||||||
isDone BOOLEAN DEFAULT false,
|
wastePile TEXT,
|
||||||
seed TEXT
|
isDone BOOLEAN DEFAULT false,
|
||||||
)
|
seed TEXT
|
||||||
|
)
|
||||||
`);
|
`);
|
||||||
stmtSOTD.run()
|
stmtSOTD.run();
|
||||||
|
|
||||||
export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`)
|
export const getSOTD = flopoDB.prepare(`SELECT *
|
||||||
export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`)
|
FROM sotd
|
||||||
export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`)
|
WHERE id = '0'`);
|
||||||
|
export const insertSOTD =
|
||||||
|
flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed)
|
||||||
|
VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`);
|
||||||
|
export const deleteSOTD = flopoDB.prepare(`DELETE
|
||||||
|
FROM sotd
|
||||||
|
WHERE id = '0'`);
|
||||||
|
|
||||||
export const stmtSOTDStats = flopoDB.prepare(`
|
export const stmtSOTDStats = flopoDB.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS sotd_stats (
|
CREATE TABLE IF NOT EXISTS sotd_stats
|
||||||
id TEXT PRIMARY KEY,
|
(
|
||||||
user_id TEXT REFERENCES users,
|
id TEXT PRIMARY KEY,
|
||||||
time INTEGER,
|
user_id TEXT REFERENCES users,
|
||||||
moves INTEGER,
|
time INTEGER,
|
||||||
score 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 getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
|
||||||
export const getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`);
|
FROM sotd_stats
|
||||||
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`);
|
JOIN users ON users.id = sotd_stats.user_id
|
||||||
export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`);
|
ORDER BY score DESC, moves ASC, time ASC`);
|
||||||
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`);
|
export const getUserSOTDStats = flopoDB.prepare(`SELECT *
|
||||||
|
FROM sotd_stats
|
||||||
|
WHERE user_id = ?`);
|
||||||
|
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score)
|
||||||
|
VALUES (@id, @user_id, @time, @moves, @score)`);
|
||||||
|
export const clearSOTDStats = flopoDB.prepare(`DELETE
|
||||||
|
FROM sotd_stats`);
|
||||||
|
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE
|
||||||
|
FROM sotd_stats
|
||||||
|
WHERE user_id = ?`);
|
||||||
|
|
||||||
export async function pruneOldLogs() {
|
export async function pruneOldLogs() {
|
||||||
const users = flopoDB.prepare(`
|
const users = flopoDB
|
||||||
SELECT user_id
|
.prepare(
|
||||||
FROM logs
|
`
|
||||||
GROUP BY user_id
|
SELECT user_id
|
||||||
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
FROM logs
|
||||||
`).all();
|
GROUP BY user_id
|
||||||
|
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
|
||||||
const transaction = flopoDB.transaction(() => {
|
const transaction = flopoDB.transaction(() => {
|
||||||
for (const { user_id } of users) {
|
for (const { user_id } of users) {
|
||||||
flopoDB.prepare(`
|
flopoDB
|
||||||
DELETE FROM logs
|
.prepare(
|
||||||
WHERE id IN (
|
`
|
||||||
SELECT id FROM (
|
DELETE
|
||||||
SELECT id,
|
FROM logs
|
||||||
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
|
WHERE id IN (SELECT id
|
||||||
FROM logs
|
FROM (SELECT id,
|
||||||
WHERE user_id = ?
|
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
|
||||||
)
|
FROM logs
|
||||||
WHERE rn > ${process.env.LOGS_BY_USER}
|
WHERE user_id = ?)
|
||||||
)
|
WHERE rn > ${process.env.LOGS_BY_USER})
|
||||||
`).run(user_id);
|
`,
|
||||||
}
|
)
|
||||||
});
|
.run(user_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
transaction()
|
transaction();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,384 +2,425 @@
|
|||||||
// Core blackjack helpers for a single continuous room.
|
// Core blackjack helpers for a single continuous room.
|
||||||
// Inspired by your poker helpers API style.
|
// Inspired by your poker helpers API style.
|
||||||
|
|
||||||
import {emitToast} from "../server/socket.js";
|
import { emitToast } from "../server/socket.js";
|
||||||
import {getUser, insertLog, updateUserCoins} from "../database/index.js";
|
import { getUser, insertLog, updateUserCoins } from "../database/index.js";
|
||||||
import {client} from "../bot/client.js";
|
import { client } from "../bot/client.js";
|
||||||
import {EmbedBuilder} from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
|
|
||||||
export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"];
|
export const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
|
||||||
export const SUITS = ["d","s","c","h"];
|
export const SUITS = ["d", "s", "c", "h"];
|
||||||
|
|
||||||
// Build a single 52-card deck like "Ad","Ts", etc.
|
// Build a single 52-card deck like "Ad","Ts", etc.
|
||||||
export const singleDeck = RANKS.flatMap(r => SUITS.map(s => `${r}${s}`));
|
export const singleDeck = RANKS.flatMap((r) => SUITS.map((s) => `${r}${s}`));
|
||||||
|
|
||||||
export function buildShoe(decks = 6) {
|
export function buildShoe(decks = 6) {
|
||||||
const shoe = [];
|
const shoe = [];
|
||||||
for (let i = 0; i < decks; i++) shoe.push(...singleDeck);
|
for (let i = 0; i < decks; i++) shoe.push(...singleDeck);
|
||||||
return shuffle(shoe);
|
return shuffle(shoe);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shuffle(arr) {
|
export function shuffle(arr) {
|
||||||
// Fisher–Yates
|
// Fisher–Yates
|
||||||
const a = [...arr];
|
const a = [...arr];
|
||||||
for (let i = a.length - 1; i > 0; i--) {
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[a[i], a[j]] = [a[j], a[i]];
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw one card from the shoe; if empty, caller should reshuffle at end of round.
|
// Draw one card from the shoe; if empty, caller should reshuffle at end of round.
|
||||||
export function draw(shoe) {
|
export function draw(shoe) {
|
||||||
return shoe.pop();
|
return shoe.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an object describing the best value of a hand with flexible Aces.
|
// Return an object describing the best value of a hand with flexible Aces.
|
||||||
export function handValue(cards) {
|
export function handValue(cards) {
|
||||||
// Count with all aces as 11, then reduce as needed
|
// Count with all aces as 11, then reduce as needed
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let aces = 0;
|
let aces = 0;
|
||||||
for (const c of cards) {
|
for (const c of cards) {
|
||||||
const r = c[0];
|
const r = c[0];
|
||||||
if (r === "A") { total += 11; aces += 1; }
|
if (r === "A") {
|
||||||
else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
|
total += 11;
|
||||||
else total += Number(r);
|
aces += 1;
|
||||||
}
|
} else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
|
||||||
while (total > 21 && aces > 0) {
|
else total += Number(r);
|
||||||
total -= 10; // convert an Ace from 11 to 1
|
}
|
||||||
aces -= 1;
|
while (total > 21 && aces > 0) {
|
||||||
}
|
total -= 10; // convert an Ace from 11 to 1
|
||||||
const soft = (aces > 0); // if any Ace still counted as 11, it's a soft hand
|
aces -= 1;
|
||||||
return { total, soft };
|
}
|
||||||
|
const soft = aces > 0; // if any Ace still counted as 11, it's a soft hand
|
||||||
|
return { total, soft };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBlackjack(cards) {
|
export function isBlackjack(cards) {
|
||||||
return cards.length === 2 && handValue(cards).total === 21;
|
return cards.length === 2 && handValue(cards).total === 21;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBust(cards) {
|
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).
|
// Dealer draw rule. By default, dealer stands on soft 17 (S17).
|
||||||
export function dealerShouldHit(dealerCards, hitSoft17 = false) {
|
export function dealerShouldHit(dealerCards, hitSoft17 = false) {
|
||||||
const v = handValue(dealerCards);
|
const v = handValue(dealerCards);
|
||||||
if (v.total < 17) return true;
|
if (v.total < 17) return true;
|
||||||
if (v.total === 17 && v.soft && hitSoft17) return true;
|
if (v.total === 17 && v.soft && hitSoft17) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare a player hand to dealer and return outcome.
|
// Compare a player hand to dealer and return outcome.
|
||||||
export function compareHands(playerCards, dealerCards) {
|
export function compareHands(playerCards, dealerCards) {
|
||||||
const pv = handValue(playerCards).total;
|
const pv = handValue(playerCards).total;
|
||||||
const dv = handValue(dealerCards).total;
|
const dv = handValue(dealerCards).total;
|
||||||
if (pv > 21) return "lose";
|
if (pv > 21) return "lose";
|
||||||
if (dv > 21) return "win";
|
if (dv > 21) return "win";
|
||||||
if (pv > dv) return "win";
|
if (pv > dv) return "win";
|
||||||
if (pv < dv) return "lose";
|
if (pv < dv) return "lose";
|
||||||
return "push";
|
return "push";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute payout for a single finished hand (no splits here).
|
// Compute payout for a single finished hand (no splits here).
|
||||||
// options: { blackjackPayout: 1.5, allowSurrender: false }
|
// options: { blackjackPayout: 1.5, allowSurrender: false }
|
||||||
export function settleHand({ bet, playerCards, dealerCards, doubled = false, surrendered = false, blackjackPayout = 1.5 }) {
|
export function settleHand({
|
||||||
if (surrendered) return { delta: -bet / 2, result: "surrender" };
|
bet,
|
||||||
|
playerCards,
|
||||||
|
dealerCards,
|
||||||
|
doubled = false,
|
||||||
|
surrendered = false,
|
||||||
|
blackjackPayout = 1.5,
|
||||||
|
}) {
|
||||||
|
if (surrendered) return { delta: -bet / 2, result: "surrender" };
|
||||||
|
|
||||||
const pBJ = isBlackjack(playerCards);
|
const pBJ = isBlackjack(playerCards);
|
||||||
const dBJ = isBlackjack(dealerCards);
|
const dBJ = isBlackjack(dealerCards);
|
||||||
|
|
||||||
if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" };
|
if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" };
|
||||||
if (!pBJ && dBJ) return { delta: -bet, result: "lose" };
|
if (!pBJ && dBJ) return { delta: -bet, result: "lose" };
|
||||||
if (pBJ && dBJ) return { delta: 0, result: "push" };
|
if (pBJ && dBJ) return { delta: 0, result: "push" };
|
||||||
|
|
||||||
const outcome = compareHands(playerCards, dealerCards);
|
const outcome = compareHands(playerCards, dealerCards);
|
||||||
let unit = bet;
|
let unit = bet;
|
||||||
if (outcome === "win") return { delta: unit, result: "win" };
|
if (outcome === "win") return { delta: unit, result: "win" };
|
||||||
if (outcome === "lose") return { delta: -unit, result: "lose" };
|
if (outcome === "lose") return { delta: -unit, result: "lose" };
|
||||||
return { delta: 0, result: "push" };
|
return { delta: 0, result: "push" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to decide if doubling is still allowed (first decision, 2 cards, not hit yet).
|
// Helper to decide if doubling is still allowed (first decision, 2 cards, not hit yet).
|
||||||
export function canDouble(hand) {
|
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
|
// Very small utility to format a public-safe snapshot of room state
|
||||||
export function publicPlayerView(player) {
|
export function publicPlayerView(player) {
|
||||||
// Hide hole cards until dealer reveal is fine for dealer only; player cards are visible.
|
// Hide hole cards until dealer reveal is fine for dealer only; player cards are visible.
|
||||||
return {
|
return {
|
||||||
id: player.id,
|
id: player.id,
|
||||||
globalName: player.globalName,
|
globalName: player.globalName,
|
||||||
avatar: player.avatar,
|
avatar: player.avatar,
|
||||||
bank: player.bank,
|
bank: player.bank,
|
||||||
currentBet: player.currentBet,
|
currentBet: player.currentBet,
|
||||||
inRound: player.inRound,
|
inRound: player.inRound,
|
||||||
hands: player.hands.map(h => ({
|
hands: player.hands.map((h) => ({
|
||||||
cards: h.cards,
|
cards: h.cards,
|
||||||
stood: h.stood,
|
stood: h.stood,
|
||||||
busted: h.busted,
|
busted: h.busted,
|
||||||
doubled: h.doubled,
|
doubled: h.doubled,
|
||||||
surrendered: h.surrendered,
|
surrendered: h.surrendered,
|
||||||
result: h.result ?? null,
|
result: h.result ?? null,
|
||||||
total: handValue(h.cards).total,
|
total: handValue(h.cards).total,
|
||||||
soft: handValue(h.cards).soft,
|
soft: handValue(h.cards).soft,
|
||||||
bet: h.bet,
|
bet: h.bet,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build initial room object
|
// Build initial room object
|
||||||
export function createBlackjackRoom({
|
export function createBlackjackRoom({
|
||||||
minBet = 10,
|
minBet = 10,
|
||||||
maxBet = 10000,
|
maxBet = 10000,
|
||||||
fakeMoney = false,
|
fakeMoney = false,
|
||||||
decks = 6,
|
decks = 6,
|
||||||
hitSoft17 = false,
|
hitSoft17 = false,
|
||||||
blackjackPayout = 1.5,
|
blackjackPayout = 1.5,
|
||||||
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
|
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
|
||||||
phaseDurations = {
|
phaseDurations = {
|
||||||
bettingMs: 15000,
|
bettingMs: 15000,
|
||||||
dealMs: 1000,
|
dealMs: 1000,
|
||||||
playMsPerPlayer: 20000,
|
playMsPerPlayer: 20000,
|
||||||
revealMs: 1000,
|
revealMs: 1000,
|
||||||
payoutMs: 10000,
|
payoutMs: 10000,
|
||||||
},
|
},
|
||||||
animation = {
|
animation = {
|
||||||
dealerDrawMs: 500,
|
dealerDrawMs: 500,
|
||||||
}
|
},
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
id: "blackjack-room",
|
id: "blackjack-room",
|
||||||
name: "Blackjack",
|
name: "Blackjack",
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
|
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
|
||||||
phase_ends_at: Date.now() + phaseDurations.bettingMs,
|
phase_ends_at: Date.now() + phaseDurations.bettingMs,
|
||||||
minBet, maxBet, fakeMoney,
|
minBet,
|
||||||
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation },
|
maxBet,
|
||||||
shoe: buildShoe(decks),
|
fakeMoney,
|
||||||
discard: [],
|
settings: {
|
||||||
dealer: { cards: [], holeHidden: true },
|
decks,
|
||||||
players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 }
|
hitSoft17,
|
||||||
leavingAfterRound: {},
|
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
|
// Reshuffle at start of the next round if the shoe is low
|
||||||
export function needsReshuffle(room) {
|
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 ---
|
// --- Round Lifecycle helpers ---
|
||||||
|
|
||||||
export function resetForNewRound(room) {
|
export function resetForNewRound(room) {
|
||||||
room.status = "betting";
|
room.status = "betting";
|
||||||
room.dealer = { cards: [], holeHidden: true };
|
room.dealer = { cards: [], holeHidden: true };
|
||||||
room.leavingAfterRound = {};
|
room.leavingAfterRound = {};
|
||||||
// Clear per-round attributes on players, but keep bank and presence
|
// Clear per-round attributes on players, but keep bank and presence
|
||||||
for (const p of Object.values(room.players)) {
|
for (const p of Object.values(room.players)) {
|
||||||
p.inRound = false;
|
p.inRound = false;
|
||||||
p.currentBet = 0;
|
p.currentBet = 0;
|
||||||
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 } ];
|
p.hands = [
|
||||||
p.activeHand = 0;
|
{
|
||||||
}
|
cards: [],
|
||||||
|
stood: false,
|
||||||
|
busted: false,
|
||||||
|
doubled: false,
|
||||||
|
surrendered: false,
|
||||||
|
hasActed: false,
|
||||||
|
bet: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
p.activeHand = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startBetting(room, now) {
|
export function startBetting(room, now) {
|
||||||
resetForNewRound(room);
|
resetForNewRound(room);
|
||||||
if (needsReshuffle(room)) {
|
if (needsReshuffle(room)) {
|
||||||
room.status = "shuffle";
|
room.status = "shuffle";
|
||||||
// quick shuffle animation phase
|
// quick shuffle animation phase
|
||||||
room.shoe = buildShoe(room.settings.decks);
|
room.shoe = buildShoe(room.settings.decks);
|
||||||
}
|
}
|
||||||
room.status = "betting";
|
room.status = "betting";
|
||||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dealInitial(room) {
|
export function dealInitial(room) {
|
||||||
room.status = "dealing";
|
room.status = "dealing";
|
||||||
// Deal one to each player who placed a bet, then again, then dealer up + hole
|
// Deal one to each player who placed a bet, then again, then dealer up + hole
|
||||||
const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet);
|
const actives = Object.values(room.players).filter((p) => p.currentBet >= room.minBet);
|
||||||
for (const p of actives) {
|
for (const p of actives) {
|
||||||
p.inRound = true;
|
p.inRound = true;
|
||||||
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: p.currentBet } ];
|
p.hands = [
|
||||||
}
|
{
|
||||||
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
|
cards: [draw(room.shoe)],
|
||||||
room.dealer.holeHidden = true;
|
stood: false,
|
||||||
for (const p of actives) {
|
busted: false,
|
||||||
p.hands[0].cards.push(draw(room.shoe));
|
doubled: false,
|
||||||
}
|
surrendered: false,
|
||||||
room.status = "playing";
|
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) {
|
export function autoActions(room) {
|
||||||
// Auto-stand if player already blackjack
|
// Auto-stand if player already blackjack
|
||||||
for (const p of Object.values(room.players)) {
|
for (const p of Object.values(room.players)) {
|
||||||
if (!p.inRound) continue;
|
if (!p.inRound) continue;
|
||||||
const h = p.hands[p.activeHand];
|
const h = p.hands[p.activeHand];
|
||||||
if (isBlackjack(h.cards)) {
|
if (isBlackjack(h.cards)) {
|
||||||
h.stood = true;
|
h.stood = true;
|
||||||
h.hasActed = true;
|
h.hasActed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function everyoneDone(room) {
|
export function everyoneDone(room) {
|
||||||
return Object.values(room.players).every(p => {
|
return Object.values(room.players).every((p) => {
|
||||||
if (!p.inRound) return true;
|
if (!p.inRound) return true;
|
||||||
return p.hands.filter(h => !h.stood && !h.busted && !h.surrendered)?.length === 0;
|
return p.hands.filter((h) => !h.stood && !h.busted && !h.surrendered)?.length === 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dealerPlay(room) {
|
export function dealerPlay(room) {
|
||||||
room.status = "dealer";
|
room.status = "dealer";
|
||||||
room.dealer.holeHidden = false;
|
room.dealer.holeHidden = false;
|
||||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||||
room.dealer.cards.push(draw(room.shoe));
|
room.dealer.cards.push(draw(room.shoe));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function settleAll(room) {
|
export async function settleAll(room) {
|
||||||
room.status = "payout";
|
room.status = "payout";
|
||||||
const allRes = {}
|
const allRes = {};
|
||||||
for (const p of Object.values(room.players)) {
|
for (const p of Object.values(room.players)) {
|
||||||
if (!p.inRound) continue;
|
if (!p.inRound) continue;
|
||||||
for (const hand of p.hands) {
|
for (const hand of p.hands) {
|
||||||
const res = settleHand({
|
const res = settleHand({
|
||||||
bet: hand.bet,
|
bet: hand.bet,
|
||||||
playerCards: hand.cards,
|
playerCards: hand.cards,
|
||||||
dealerCards: room.dealer.cards,
|
dealerCards: room.dealer.cards,
|
||||||
doubled: hand.doubled,
|
doubled: hand.doubled,
|
||||||
surrendered: hand.surrendered,
|
surrendered: hand.surrendered,
|
||||||
blackjackPayout: room.settings.blackjackPayout,
|
blackjackPayout: room.settings.blackjackPayout,
|
||||||
});
|
});
|
||||||
if (allRes[p.id]) {
|
if (allRes[p.id]) {
|
||||||
allRes[p.id].push(res);
|
allRes[p.id].push(res);
|
||||||
} else {
|
} else {
|
||||||
allRes[p.id] = [res];
|
allRes[p.id] = [res];
|
||||||
}
|
}
|
||||||
|
|
||||||
p.totalDelta += res.delta
|
p.totalDelta += res.delta;
|
||||||
p.totalBets++
|
p.totalBets++;
|
||||||
if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') {
|
if (res.result === "win" || res.result === "push" || res.result === "blackjack") {
|
||||||
const userDB = getUser.get(p.id);
|
const userDB = getUser.get(p.id);
|
||||||
if (userDB) {
|
if (userDB) {
|
||||||
const coins = userDB.coins;
|
const coins = userDB.coins;
|
||||||
try {
|
try {
|
||||||
updateUserCoins.run({ id: p.id, coins: coins + hand.bet + res.delta });
|
updateUserCoins.run({
|
||||||
insertLog.run({
|
id: p.id,
|
||||||
id: `${p.id}-blackjack-${Date.now()}`,
|
coins: coins + hand.bet + res.delta,
|
||||||
user_id: p.id, target_user_id: null,
|
});
|
||||||
action: 'BLACKJACK_PAYOUT',
|
insertLog.run({
|
||||||
coins_amount: res.delta + hand.bet, user_new_amount: coins + hand.bet + res.delta,
|
id: `${p.id}-blackjack-${Date.now()}`,
|
||||||
});
|
user_id: p.id,
|
||||||
p.bank = coins + hand.bet + res.delta
|
target_user_id: null,
|
||||||
} catch (e) {
|
action: "BLACKJACK_PAYOUT",
|
||||||
console.log(e)
|
coins_amount: res.delta + hand.bet,
|
||||||
}
|
user_new_amount: coins + hand.bet + res.delta,
|
||||||
}
|
});
|
||||||
}
|
p.bank = coins + hand.bet + res.delta;
|
||||||
emitToast({ type: `payout-res`, allRes });
|
} catch (e) {
|
||||||
hand.result = res.result;
|
console.log(e);
|
||||||
hand.delta = res.delta;
|
}
|
||||||
try {
|
}
|
||||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
}
|
||||||
const generalChannel = guild.channels.cache.find(
|
emitToast({ type: `payout-res`, allRes });
|
||||||
ch => ch.name === 'général' || ch.name === 'general'
|
hand.result = res.result;
|
||||||
);
|
hand.delta = res.delta;
|
||||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
try {
|
||||||
const updatedEmbed = new EmbedBuilder()
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||||
.addFields(
|
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||||
{
|
const updatedEmbed = new EmbedBuilder()
|
||||||
name: `Gains`,
|
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||||
value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`,
|
.addFields(
|
||||||
inline: true
|
{
|
||||||
},
|
name: `Gains`,
|
||||||
{
|
value: `**${p.totalDelta >= 0 ? "+" + p.totalDelta : p.totalDelta}** Flopos`,
|
||||||
name: `Mises jouées`,
|
inline: true,
|
||||||
value: `**${p.totalBets}**`,
|
},
|
||||||
inline: true
|
{
|
||||||
}
|
name: `Mises jouées`,
|
||||||
)
|
value: `**${p.totalBets}**`,
|
||||||
.setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
inline: true,
|
||||||
.setTimestamp(new Date());
|
},
|
||||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
)
|
||||||
} catch (e) {
|
.setColor(p.totalDelta >= 0 ? 0x22a55b : 0xed4245)
|
||||||
console.log(e);
|
.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.
|
// Apply a player decision; returns a string event or throws on invalid.
|
||||||
export function applyAction(room, playerId, action) {
|
export function applyAction(room, playerId, action) {
|
||||||
const p = room.players[playerId];
|
const p = room.players[playerId];
|
||||||
if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed");
|
if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed");
|
||||||
const hand = p.hands[p.activeHand];
|
const hand = p.hands[p.activeHand];
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "hit": {
|
case "hit": {
|
||||||
if (hand.stood || hand.busted) throw new Error("Already ended");
|
if (hand.stood || hand.busted) throw new Error("Already ended");
|
||||||
hand.hasActed = true;
|
hand.hasActed = true;
|
||||||
hand.cards.push(draw(room.shoe));
|
hand.cards.push(draw(room.shoe));
|
||||||
if (isBust(hand.cards)) hand.busted = true;
|
if (isBust(hand.cards)) hand.busted = true;
|
||||||
return "hit";
|
return "hit";
|
||||||
}
|
}
|
||||||
case "stand": {
|
case "stand": {
|
||||||
hand.stood = true;
|
hand.stood = true;
|
||||||
hand.hasActed = true;
|
hand.hasActed = true;
|
||||||
p.activeHand++;
|
p.activeHand++;
|
||||||
return "stand";
|
return "stand";
|
||||||
}
|
}
|
||||||
case "double": {
|
case "double": {
|
||||||
if (!canDouble(hand)) throw new Error("Cannot double now");
|
if (!canDouble(hand)) throw new Error("Cannot double now");
|
||||||
hand.doubled = true;
|
hand.doubled = true;
|
||||||
hand.bet*=2
|
hand.bet *= 2;
|
||||||
p.currentBet+=hand.bet/2
|
p.currentBet += hand.bet / 2;
|
||||||
hand.hasActed = true;
|
hand.hasActed = true;
|
||||||
// The caller (routes) must also handle additional balance lock on the bet if using real coins
|
// The caller (routes) must also handle additional balance lock on the bet if using real coins
|
||||||
hand.cards.push(draw(room.shoe));
|
hand.cards.push(draw(room.shoe));
|
||||||
if (isBust(hand.cards)) hand.busted = true;
|
if (isBust(hand.cards)) hand.busted = true;
|
||||||
else hand.stood = true;
|
else hand.stood = true;
|
||||||
p.activeHand++;
|
p.activeHand++;
|
||||||
return "double";
|
return "double";
|
||||||
}
|
}
|
||||||
case "split": {
|
case "split": {
|
||||||
if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards");
|
if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards");
|
||||||
const r0 = hand.cards[0][0];
|
const r0 = hand.cards[0][0];
|
||||||
const r1 = hand.cards[1][0];
|
const r1 = hand.cards[1][0];
|
||||||
if (r0 !== r1) throw new Error("Cannot split: cards not same rank");
|
if (r0 !== r1) throw new Error("Cannot split: cards not same rank");
|
||||||
|
|
||||||
const cardA = hand.cards[0];
|
const cardA = hand.cards[0];
|
||||||
const cardB = hand.cards[1];
|
const cardB = hand.cards[1];
|
||||||
|
|
||||||
hand.cards = [cardA];
|
hand.cards = [cardA];
|
||||||
hand.stood = false;
|
hand.stood = false;
|
||||||
hand.busted = false;
|
hand.busted = false;
|
||||||
hand.doubled = false;
|
hand.doubled = false;
|
||||||
hand.surrendered = false;
|
hand.surrendered = false;
|
||||||
hand.hasActed = false;
|
hand.hasActed = false;
|
||||||
|
|
||||||
const newHand = {
|
const newHand = {
|
||||||
cards: [cardB],
|
cards: [cardB],
|
||||||
stood: false,
|
stood: false,
|
||||||
busted: false,
|
busted: false,
|
||||||
doubled: false,
|
doubled: false,
|
||||||
surrendered: false,
|
surrendered: false,
|
||||||
hasActed: false,
|
hasActed: false,
|
||||||
bet: hand.bet,
|
bet: hand.bet,
|
||||||
}
|
};
|
||||||
|
|
||||||
p.currentBet *= 2
|
p.currentBet *= 2;
|
||||||
|
|
||||||
p.hands.splice(p.activeHand + 1, 0, newHand);
|
p.hands.splice(p.activeHand + 1, 0, newHand);
|
||||||
|
|
||||||
hand.cards.push(draw(room.shoe));
|
hand.cards.push(draw(room.shoe));
|
||||||
newHand.cards.push(draw(room.shoe));
|
newHand.cards.push(draw(room.shoe));
|
||||||
|
|
||||||
return "split";
|
return "split";
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid action");
|
throw new Error("Invalid action");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
252
src/game/elo.js
252
src/game/elo.js
@@ -1,12 +1,6 @@
|
|||||||
import {
|
import { getUser, getUserElo, insertElos, updateElo, insertGame } from "../database/index.js";
|
||||||
getUser,
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||||
getUserElo,
|
import { client } from "../bot/client.js";
|
||||||
insertElos,
|
|
||||||
updateElo,
|
|
||||||
insertGame,
|
|
||||||
} from '../database/index.js';
|
|
||||||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
|
||||||
import {client} from "../bot/client.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles Elo calculation for a standard 1v1 game.
|
* Handles Elo calculation for a standard 1v1 game.
|
||||||
@@ -17,81 +11,85 @@ import {client} from "../bot/client.js";
|
|||||||
* @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4').
|
* @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4').
|
||||||
*/
|
*/
|
||||||
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||||
// --- 1. Fetch Player Data ---
|
// --- 1. Fetch Player Data ---
|
||||||
const p1DB = getUser.get(p1Id);
|
const p1DB = getUser.get(p1Id);
|
||||||
const p2DB = getUser.get(p2Id);
|
const p2DB = getUser.get(p2Id);
|
||||||
if (!p1DB || !p2DB) {
|
if (!p1DB || !p2DB) {
|
||||||
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
|
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let p1EloData = getUserElo.get({ id: p1Id });
|
let p1EloData = getUserElo.get({ id: p1Id });
|
||||||
let p2EloData = getUserElo.get({ id: p2Id });
|
let p2EloData = getUserElo.get({ id: p2Id });
|
||||||
|
|
||||||
// --- 2. Initialize Elo if it doesn't exist ---
|
// --- 2. Initialize Elo if it doesn't exist ---
|
||||||
if (!p1EloData) {
|
if (!p1EloData) {
|
||||||
await insertElos.run({ id: p1Id, elo: 1000 });
|
await insertElos.run({ id: p1Id, elo: 1000 });
|
||||||
p1EloData = { id: p1Id, elo: 1000 };
|
p1EloData = { id: p1Id, elo: 1000 };
|
||||||
}
|
}
|
||||||
if (!p2EloData) {
|
if (!p2EloData) {
|
||||||
await insertElos.run({ id: p2Id, elo: 1000 });
|
await insertElos.run({ id: p2Id, elo: 1000 });
|
||||||
p2EloData = { id: p2Id, elo: 1000 };
|
p2EloData = { id: p2Id, elo: 1000 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const p1CurrentElo = p1EloData.elo;
|
const p1CurrentElo = p1EloData.elo;
|
||||||
const p2CurrentElo = p2EloData.elo;
|
const p2CurrentElo = p2EloData.elo;
|
||||||
|
|
||||||
// --- 3. Calculate Elo Change ---
|
// --- 3. Calculate Elo Change ---
|
||||||
// The K-factor determines how much the Elo rating changes after a game.
|
// The K-factor determines how much the Elo rating changes after a game.
|
||||||
const K_FACTOR = 32;
|
const K_FACTOR = 32;
|
||||||
|
|
||||||
// Calculate expected scores
|
// Calculate expected scores
|
||||||
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
|
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
|
||||||
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
|
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
|
||||||
|
|
||||||
// Calculate new Elo ratings
|
// Calculate new Elo ratings
|
||||||
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
|
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
|
||||||
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
|
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
|
||||||
|
|
||||||
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
|
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
|
||||||
const finalP1Elo = Math.max(0, p1NewElo);
|
const finalP1Elo = Math.max(0, p1NewElo);
|
||||||
const finalP2Elo = Math.max(0, p2NewElo);
|
const finalP2Elo = Math.max(0, p2NewElo);
|
||||||
|
|
||||||
console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`);
|
console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`);
|
||||||
console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`);
|
console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`);
|
||||||
try {
|
try {
|
||||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||||
const user1 = await client.users.fetch(p1Id);
|
const user1 = await client.users.fetch(p1Id);
|
||||||
const user2 = await client.users.fetch(p2Id);
|
const user2 = await client.users.fetch(p2Id);
|
||||||
const diff1 = finalP1Elo - p1CurrentElo;
|
const diff1 = finalP1Elo - p1CurrentElo;
|
||||||
const diff2 = finalP2Elo - p2CurrentElo;
|
const diff2 = finalP2Elo - p2CurrentElo;
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`FlopoRank - ${type}`)
|
.setTitle(`FlopoRank - ${type}`)
|
||||||
.setDescription(`
|
.setDescription(
|
||||||
**${user1.globalName || user1.username}** a ${diff1 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? '↗️' : '↘️'} **${finalP1Elo}**\n
|
`
|
||||||
**${user2.globalName || user2.username}** a ${diff2 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? '↗️' : '↘️'} **${finalP2Elo}**\n
|
**${user1.globalName || user1.username}** a ${diff1 > 0 ? "gagné" : "perdu"} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? "↗️" : "↘️"} **${finalP1Elo}**\n
|
||||||
`)
|
**${user2.globalName || user2.username}** a ${diff2 > 0 ? "gagné" : "perdu"} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? "↗️" : "↘️"} **${finalP2Elo}**\n
|
||||||
.setColor('#5865f2');
|
`,
|
||||||
await generalChannel.send({ embeds: [embed] });
|
)
|
||||||
} catch (e) { console.error(`Failed to post elo update message`, e); }
|
.setColor("#5865f2");
|
||||||
|
await generalChannel.send({ embeds: [embed] });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to post elo update message`, e);
|
||||||
|
}
|
||||||
|
|
||||||
// --- 4. Update Database ---
|
// --- 4. Update Database ---
|
||||||
updateElo.run({ id: p1Id, elo: finalP1Elo });
|
updateElo.run({ id: p1Id, elo: finalP1Elo });
|
||||||
updateElo.run({ id: p2Id, elo: finalP2Elo });
|
updateElo.run({ id: p2Id, elo: finalP2Elo });
|
||||||
|
|
||||||
insertGame.run({
|
insertGame.run({
|
||||||
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
||||||
p1: p1Id,
|
p1: p1Id,
|
||||||
p2: p2Id,
|
p2: p2Id,
|
||||||
p1_score: p1Score,
|
p1_score: p1Score,
|
||||||
p2_score: p2Score,
|
p2_score: p2Score,
|
||||||
p1_elo: p1CurrentElo,
|
p1_elo: p1CurrentElo,
|
||||||
p2_elo: p2CurrentElo,
|
p2_elo: p2CurrentElo,
|
||||||
p1_new_elo: finalP1Elo,
|
p1_new_elo: finalP1Elo,
|
||||||
p2_new_elo: finalP2Elo,
|
p2_new_elo: finalP2Elo,
|
||||||
type: type,
|
type: type,
|
||||||
timestamp: Date.now(),
|
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.
|
* @param {object} room - The poker room object containing player and winner info.
|
||||||
*/
|
*/
|
||||||
export async function pokerEloHandler(room) {
|
export async function pokerEloHandler(room) {
|
||||||
if (room.fakeMoney) {
|
if (room.fakeMoney) {
|
||||||
console.log("Skipping Elo update for fake money poker game.");
|
console.log("Skipping Elo update for fake money poker game.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerIds = Object.keys(room.players);
|
const playerIds = Object.keys(room.players);
|
||||||
if (playerIds.length < 2) return; // Not enough players to calculate Elo
|
if (playerIds.length < 2) return; // Not enough players to calculate Elo
|
||||||
|
|
||||||
// Fetch all players' Elo data at once
|
// Fetch all players' Elo data at once
|
||||||
const dbPlayers = playerIds.map(id => {
|
const dbPlayers = playerIds.map((id) => {
|
||||||
const user = getUser.get(id);
|
const user = getUser.get(id);
|
||||||
const elo = getUserElo.get({ id })?.elo || 1000;
|
const elo = getUserElo.get({ id })?.elo || 1000;
|
||||||
return { ...user, elo };
|
return { ...user, elo };
|
||||||
});
|
});
|
||||||
|
|
||||||
const winnerIds = new Set(room.winners);
|
const winnerIds = new Set(room.winners);
|
||||||
const playerCount = dbPlayers.length;
|
const playerCount = dbPlayers.length;
|
||||||
const K_BASE = 16; // A lower K-factor is often used for multi-player games
|
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 => {
|
dbPlayers.forEach((player) => {
|
||||||
// Expected score is the chance of winning against an "average" player from the field
|
// Expected score is the chance of winning against an "average" player from the field
|
||||||
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
|
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
|
||||||
|
|
||||||
// Determine actual score
|
// Determine actual score
|
||||||
let actualScore;
|
let actualScore;
|
||||||
if (winnerIds.has(player.id)) {
|
if (winnerIds.has(player.id)) {
|
||||||
// Winners share the "win" points
|
// Winners share the "win" points
|
||||||
actualScore = 1 / winnerIds.size;
|
actualScore = 1 / winnerIds.size;
|
||||||
} else {
|
} else {
|
||||||
actualScore = 0;
|
actualScore = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic K-factor: higher impact for more significant results
|
// Dynamic K-factor: higher impact for more significant results
|
||||||
const kFactor = K_BASE * playerCount;
|
const kFactor = K_BASE * playerCount;
|
||||||
const eloChange = kFactor * (actualScore - expectedScore);
|
const eloChange = kFactor * (actualScore - expectedScore);
|
||||||
const newElo = Math.max(100, Math.round(player.elo + eloChange));
|
const newElo = Math.max(100, Math.round(player.elo + eloChange));
|
||||||
|
|
||||||
if (!isNaN(newElo)) {
|
if (!isNaN(newElo)) {
|
||||||
console.log(`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`);
|
console.log(
|
||||||
updateElo.run({ id: player.id, elo: newElo });
|
`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`,
|
||||||
|
);
|
||||||
|
updateElo.run({ id: player.id, elo: newElo });
|
||||||
|
|
||||||
insertGame.run({
|
insertGame.run({
|
||||||
id: `${player.id}-poker-${Date.now()}`,
|
id: `${player.id}-poker-${Date.now()}`,
|
||||||
p1: player.id,
|
p1: player.id,
|
||||||
p2: null, // No single opponent
|
p2: null, // No single opponent
|
||||||
p1_score: actualScore,
|
p1_score: actualScore,
|
||||||
p2_score: null,
|
p2_score: null,
|
||||||
p1_elo: player.elo,
|
p1_elo: player.elo,
|
||||||
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
|
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
|
||||||
p1_new_elo: newElo,
|
p1_new_elo: newElo,
|
||||||
p2_new_elo: null,
|
p2_new_elo: null,
|
||||||
type: 'POKER_ROUND',
|
type: "POKER_ROUND",
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error calculating new Elo for ${player.globalName}.`);
|
console.error(`Error calculating new Elo for ${player.globalName}.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
getUser,
|
getUser,
|
||||||
updateUserCoins,
|
updateUserCoins,
|
||||||
insertLog,
|
insertLog,
|
||||||
getAllSkins,
|
getAllSkins,
|
||||||
insertSOTD,
|
insertSOTD,
|
||||||
clearSOTDStats,
|
clearSOTDStats,
|
||||||
getAllSOTDStats, deleteSOTD, insertGame,
|
getAllSOTDStats,
|
||||||
} from '../database/index.js';
|
deleteSOTD,
|
||||||
import { messagesTimestamps, activeSlowmodes, skins } from './state.js';
|
insertGame,
|
||||||
import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js';
|
} from "../database/index.js";
|
||||||
|
import { messagesTimestamps, activeSlowmodes, skins } from "./state.js";
|
||||||
|
import { deal, createSeededRNG, seededShuffle, createDeck } from "./solitaire.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles awarding points (coins) to users for their message activity.
|
* Handles awarding points (coins) to users for their message activity.
|
||||||
@@ -17,53 +19,53 @@ import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js
|
|||||||
* @returns {boolean} True if points were awarded, false otherwise.
|
* @returns {boolean} True if points were awarded, false otherwise.
|
||||||
*/
|
*/
|
||||||
export async function channelPointsHandler(message) {
|
export async function channelPointsHandler(message) {
|
||||||
const author = message.author;
|
const author = message.author;
|
||||||
const authorDB = getUser.get(author.id);
|
const authorDB = getUser.get(author.id);
|
||||||
|
|
||||||
if (!authorDB) {
|
if (!authorDB) {
|
||||||
// User not in our database, do nothing.
|
// User not in our database, do nothing.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore short messages or commands that might be spammed
|
// Ignore short messages or commands that might be spammed
|
||||||
if (message.content.length < 3 || message.content.startsWith('?')) {
|
if (message.content.length < 3 || message.content.startsWith("?")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const userTimestamps = messagesTimestamps.get(author.id) || [];
|
const userTimestamps = messagesTimestamps.get(author.id) || [];
|
||||||
|
|
||||||
// Filter out timestamps older than 15 minutes (900,000 ms)
|
// Filter out timestamps older than 15 minutes (900,000 ms)
|
||||||
const recentTimestamps = userTimestamps.filter(ts => now - ts < 900000);
|
const recentTimestamps = userTimestamps.filter((ts) => now - ts < 900000);
|
||||||
|
|
||||||
// If the user has already sent 10 messages in the last 15 mins, do nothing
|
// If the user has already sent 10 messages in the last 15 mins, do nothing
|
||||||
if (recentTimestamps.length >= 10) {
|
if (recentTimestamps.length >= 10) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new message timestamp
|
// Add the new message timestamp
|
||||||
recentTimestamps.push(now);
|
recentTimestamps.push(now);
|
||||||
messagesTimestamps.set(author.id, recentTimestamps);
|
messagesTimestamps.set(author.id, recentTimestamps);
|
||||||
|
|
||||||
// Award 50 coins for the 10th message, 10 for others
|
// Award 50 coins for the 10th message, 10 for others
|
||||||
const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10;
|
const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10;
|
||||||
const newCoinTotal = authorDB.coins + coinsToAdd;
|
const newCoinTotal = authorDB.coins + coinsToAdd;
|
||||||
|
|
||||||
updateUserCoins.run({
|
updateUserCoins.run({
|
||||||
id: author.id,
|
id: author.id,
|
||||||
coins: newCoinTotal,
|
coins: newCoinTotal,
|
||||||
});
|
});
|
||||||
|
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${author.id}-${now}`,
|
id: `${author.id}-${now}`,
|
||||||
user_id: author.id,
|
user_id: author.id,
|
||||||
action: 'AUTO_COINS',
|
action: "AUTO_COINS",
|
||||||
target_user_id: null,
|
target_user_id: null,
|
||||||
coins_amount: coinsToAdd,
|
coins_amount: coinsToAdd,
|
||||||
user_new_amount: newCoinTotal,
|
user_new_amount: newCoinTotal,
|
||||||
});
|
});
|
||||||
|
|
||||||
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.
|
* @returns {object} An object indicating if a message was deleted or a slowmode expired.
|
||||||
*/
|
*/
|
||||||
export async function slowmodesHandler(message) {
|
export async function slowmodesHandler(message) {
|
||||||
const author = message.author;
|
const author = message.author;
|
||||||
const authorSlowmode = activeSlowmodes[author.id];
|
const authorSlowmode = activeSlowmodes[author.id];
|
||||||
|
|
||||||
if (!authorSlowmode) {
|
if (!authorSlowmode) {
|
||||||
return { deleted: false, expired: false };
|
return { deleted: false, expired: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Check if the slowmode duration has passed
|
// Check if the slowmode duration has passed
|
||||||
if (now > authorSlowmode.endAt) {
|
if (now > authorSlowmode.endAt) {
|
||||||
console.log(`Slowmode for ${author.username} has expired.`);
|
console.log(`Slowmode for ${author.username} has expired.`);
|
||||||
delete activeSlowmodes[author.id];
|
delete activeSlowmodes[author.id];
|
||||||
return { deleted: false, expired: true };
|
return { deleted: false, expired: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is messaging too quickly (less than 1 minute between messages)
|
// Check if the user is messaging too quickly (less than 1 minute between messages)
|
||||||
if (authorSlowmode.lastMessage && (now - authorSlowmode.lastMessage < 60 * 1000)) {
|
if (authorSlowmode.lastMessage && now - authorSlowmode.lastMessage < 60 * 1000) {
|
||||||
try {
|
try {
|
||||||
await message.delete();
|
await message.delete();
|
||||||
console.log(`Deleted a message from slowmoded user: ${author.username}`);
|
console.log(`Deleted a message from slowmoded user: ${author.username}`);
|
||||||
return { deleted: true, expired: false };
|
return { deleted: true, expired: false };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to delete slowmode message:`, err);
|
console.error(`Failed to delete slowmode message:`, err);
|
||||||
return { deleted: false, expired: false };
|
return { deleted: false, expired: false };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update the last message timestamp for the user
|
// Update the last message timestamp for the user
|
||||||
authorSlowmode.lastMessage = now;
|
authorSlowmode.lastMessage = now;
|
||||||
return { deleted: false, expired: false };
|
return { deleted: false, expired: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,27 +113,27 @@ export async function slowmodesHandler(message) {
|
|||||||
* @returns {string} The calculated random price as a string.
|
* @returns {string} The calculated random price as a string.
|
||||||
*/
|
*/
|
||||||
export function randomSkinPrice() {
|
export function randomSkinPrice() {
|
||||||
const dbSkins = getAllSkins.all();
|
const dbSkins = getAllSkins.all();
|
||||||
if (dbSkins.length === 0) return '0.00';
|
if (dbSkins.length === 0) return "0.00";
|
||||||
|
|
||||||
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
|
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
|
||||||
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
|
const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid);
|
||||||
|
|
||||||
if (!randomSkinData) return '0.00';
|
if (!randomSkinData) return "0.00";
|
||||||
|
|
||||||
// Generate random level and chroma
|
// Generate random level and chroma
|
||||||
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1;
|
||||||
let randomChroma = 1;
|
let randomChroma = 1;
|
||||||
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) {
|
||||||
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate price based on these random values
|
// Calculate price based on these random values
|
||||||
let result = parseFloat(randomDbSkin.basePrice);
|
let result = parseFloat(randomDbSkin.basePrice);
|
||||||
result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2)));
|
result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2);
|
||||||
result *= (1 + (randomChroma / 4));
|
result *= 1 + randomChroma / 4;
|
||||||
|
|
||||||
return result.toFixed(0);
|
return result.toFixed(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,68 +141,70 @@ export function randomSkinPrice() {
|
|||||||
* This function clears previous stats, awards the winner, and generates a new daily seed.
|
* This function clears previous stats, awards the winner, and generates a new daily seed.
|
||||||
*/
|
*/
|
||||||
export function initTodaysSOTD() {
|
export function initTodaysSOTD() {
|
||||||
console.log('Initializing new Solitaire of the Day...');
|
console.log("Initializing new Solitaire of the Day...");
|
||||||
|
|
||||||
// 1. Award previous day's winner
|
// 1. Award previous day's winner
|
||||||
const rankings = getAllSOTDStats.all();
|
const rankings = getAllSOTDStats.all();
|
||||||
if (rankings.length > 0) {
|
if (rankings.length > 0) {
|
||||||
const winnerId = rankings[0].user_id;
|
const winnerId = rankings[0].user_id;
|
||||||
const winnerUser = getUser.get(winnerId);
|
const winnerUser = getUser.get(winnerId);
|
||||||
|
|
||||||
if (winnerUser) {
|
if (winnerUser) {
|
||||||
const reward = 1000;
|
const reward = 1000;
|
||||||
const newCoinTotal = winnerUser.coins + reward;
|
const newCoinTotal = winnerUser.coins + reward;
|
||||||
updateUserCoins.run({ id: winnerId, coins: newCoinTotal });
|
updateUserCoins.run({ id: winnerId, coins: newCoinTotal });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${winnerId}-sotd-win-${Date.now()}`,
|
id: `${winnerId}-sotd-win-${Date.now()}`,
|
||||||
target_user_id: null,
|
target_user_id: null,
|
||||||
user_id: winnerId,
|
user_id: winnerId,
|
||||||
action: 'SOTD_FIRST_PLACE',
|
action: "SOTD_FIRST_PLACE",
|
||||||
coins_amount: reward,
|
coins_amount: reward,
|
||||||
user_new_amount: newCoinTotal,
|
user_new_amount: newCoinTotal,
|
||||||
});
|
});
|
||||||
console.log(`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`);
|
console.log(
|
||||||
insertGame.run({
|
`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`,
|
||||||
id: `${winnerId}-${Date.now()}`,
|
);
|
||||||
p1: winnerId,
|
insertGame.run({
|
||||||
p2: null,
|
id: `${winnerId}-${Date.now()}`,
|
||||||
p1_score: rankings[0].score,
|
p1: winnerId,
|
||||||
p2_score: null,
|
p2: null,
|
||||||
p1_elo: winnerUser.elo,
|
p1_score: rankings[0].score,
|
||||||
p2_elo: null,
|
p2_score: null,
|
||||||
p1_new_elo: winnerUser.elo,
|
p1_elo: winnerUser.elo,
|
||||||
p2_new_elo: null,
|
p2_elo: null,
|
||||||
type: 'SOTD',
|
p1_new_elo: winnerUser.elo,
|
||||||
timestamp: Date.now(),
|
p2_new_elo: null,
|
||||||
});
|
type: "SOTD",
|
||||||
}
|
timestamp: Date.now(),
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Generate a new seeded deck for today
|
// 2. Generate a new seeded deck for today
|
||||||
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
let numericSeed = 0;
|
let numericSeed = 0;
|
||||||
for (let i = 0; i < newRandomSeed.length; i++) {
|
for (let i = 0; i < newRandomSeed.length; i++) {
|
||||||
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF;
|
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xffffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rng = createSeededRNG(numericSeed);
|
const rng = createSeededRNG(numericSeed);
|
||||||
const deck = createDeck();
|
const deck = createDeck();
|
||||||
const shuffledDeck = seededShuffle(deck, rng);
|
const shuffledDeck = seededShuffle(deck, rng);
|
||||||
const todaysSOTD = deal(shuffledDeck);
|
const todaysSOTD = deal(shuffledDeck);
|
||||||
|
|
||||||
// 3. Clear old stats and save the new game state to the database
|
// 3. Clear old stats and save the new game state to the database
|
||||||
try {
|
try {
|
||||||
clearSOTDStats.run();
|
clearSOTDStats.run();
|
||||||
deleteSOTD.run();
|
deleteSOTD.run();
|
||||||
insertSOTD.run({
|
insertSOTD.run({
|
||||||
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
|
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
|
||||||
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
|
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
|
||||||
stockPile: JSON.stringify(todaysSOTD.stockPile),
|
stockPile: JSON.stringify(todaysSOTD.stockPile),
|
||||||
wastePile: JSON.stringify(todaysSOTD.wastePile),
|
wastePile: JSON.stringify(todaysSOTD.wastePile),
|
||||||
seed: newRandomSeed,
|
seed: newRandomSeed,
|
||||||
});
|
});
|
||||||
console.log("Today's SOTD is ready with a new seed.");
|
console.log("Today's SOTD is ready with a new seed.");
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.error("Error saving new SOTD to database:", e);
|
console.error("Error saving new SOTD to database:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,60 @@
|
|||||||
import pkg from 'pokersolver';
|
import pkg from "pokersolver";
|
||||||
const { Hand } = pkg;
|
const { Hand } = pkg;
|
||||||
|
|
||||||
// An array of all 52 standard playing cards.
|
// An array of all 52 standard playing cards.
|
||||||
export const initialCards = [
|
export const initialCards = [
|
||||||
'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd',
|
"Ad",
|
||||||
'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks',
|
"2d",
|
||||||
'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc',
|
"3d",
|
||||||
'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh',
|
"4d",
|
||||||
|
"5d",
|
||||||
|
"6d",
|
||||||
|
"7d",
|
||||||
|
"8d",
|
||||||
|
"9d",
|
||||||
|
"Td",
|
||||||
|
"Jd",
|
||||||
|
"Qd",
|
||||||
|
"Kd",
|
||||||
|
"As",
|
||||||
|
"2s",
|
||||||
|
"3s",
|
||||||
|
"4s",
|
||||||
|
"5s",
|
||||||
|
"6s",
|
||||||
|
"7s",
|
||||||
|
"8s",
|
||||||
|
"9s",
|
||||||
|
"Ts",
|
||||||
|
"Js",
|
||||||
|
"Qs",
|
||||||
|
"Ks",
|
||||||
|
"Ac",
|
||||||
|
"2c",
|
||||||
|
"3c",
|
||||||
|
"4c",
|
||||||
|
"5c",
|
||||||
|
"6c",
|
||||||
|
"7c",
|
||||||
|
"8c",
|
||||||
|
"9c",
|
||||||
|
"Tc",
|
||||||
|
"Jc",
|
||||||
|
"Qc",
|
||||||
|
"Kc",
|
||||||
|
"Ah",
|
||||||
|
"2h",
|
||||||
|
"3h",
|
||||||
|
"4h",
|
||||||
|
"5h",
|
||||||
|
"6h",
|
||||||
|
"7h",
|
||||||
|
"8h",
|
||||||
|
"9h",
|
||||||
|
"Th",
|
||||||
|
"Jh",
|
||||||
|
"Qh",
|
||||||
|
"Kh",
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,8 +62,8 @@ export const initialCards = [
|
|||||||
* @returns {Array<string>} A new array containing all 52 cards in a random order.
|
* @returns {Array<string>} A new array containing all 52 cards in a random order.
|
||||||
*/
|
*/
|
||||||
export function initialShuffledCards() {
|
export function initialShuffledCards() {
|
||||||
// Create a copy and sort it randomly
|
// Create a copy and sort it randomly
|
||||||
return [...initialCards].sort(() => 0.5 - Math.random());
|
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.
|
* @returns {string|null} The ID of the next player, or null if none is found.
|
||||||
*/
|
*/
|
||||||
export function getFirstActivePlayerAfterDealer(room) {
|
export function getFirstActivePlayerAfterDealer(room) {
|
||||||
const players = Object.values(room.players);
|
const players = Object.values(room.players);
|
||||||
const dealerPosition = players.findIndex((p) => p.id === room.dealer);
|
const dealerPosition = players.findIndex((p) => p.id === room.dealer);
|
||||||
|
|
||||||
// Loop through players starting from the one after the dealer
|
// Loop through players starting from the one after the dealer
|
||||||
for (let i = 1; i <= players.length; i++) {
|
for (let i = 1; i <= players.length; i++) {
|
||||||
const nextPos = (dealerPosition + i) % players.length;
|
const nextPos = (dealerPosition + i) % players.length;
|
||||||
const nextPlayer = players[nextPos];
|
const nextPlayer = players[nextPos];
|
||||||
// Player must not be folded or all-in to be able to act
|
// Player must not be folded or all-in to be able to act
|
||||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||||
return nextPlayer.id;
|
return nextPlayer.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // Should not happen in a normal game
|
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.
|
* @returns {string|null} The ID of the next player, or null if none is found.
|
||||||
*/
|
*/
|
||||||
export function getNextActivePlayer(room) {
|
export function getNextActivePlayer(room) {
|
||||||
const players = Object.values(room.players);
|
const players = Object.values(room.players);
|
||||||
const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player);
|
const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player);
|
||||||
|
|
||||||
// Loop through players starting from the one after the current player
|
// Loop through players starting from the one after the current player
|
||||||
for (let i = 1; i <= players.length; i++) {
|
for (let i = 1; i <= players.length; i++) {
|
||||||
const nextPos = (currentPlayerPosition + i) % players.length;
|
const nextPos = (currentPlayerPosition + i) % players.length;
|
||||||
const nextPlayer = players[nextPos];
|
const nextPlayer = players[nextPos];
|
||||||
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) {
|
||||||
return nextPlayer.id;
|
return nextPlayer.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,40 +114,54 @@ export function getNextActivePlayer(room) {
|
|||||||
* @returns {object} An object with `endRound`, `winner`, and `nextPhase` properties.
|
* @returns {object} An object with `endRound`, `winner`, and `nextPhase` properties.
|
||||||
*/
|
*/
|
||||||
export function checkEndOfBettingRound(room) {
|
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) ---
|
// --- Scenario 1: Only one player left (everyone else folded) ---
|
||||||
if (activePlayers.length === 1) {
|
if (activePlayers.length === 1) {
|
||||||
return { endRound: true, winner: activePlayers[0].id, nextPhase: 'showdown' };
|
return {
|
||||||
}
|
endRound: true,
|
||||||
|
winner: activePlayers[0].id,
|
||||||
|
nextPhase: "showdown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- Scenario 2: All remaining players are all-in ---
|
// --- Scenario 2: All remaining players are all-in ---
|
||||||
// The hand goes immediately to a "progressive showdown".
|
// The hand goes immediately to a "progressive showdown".
|
||||||
const allInPlayers = activePlayers.filter(p => p.allin);
|
const allInPlayers = activePlayers.filter((p) => p.allin);
|
||||||
if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) {
|
if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) {
|
||||||
return { endRound: true, winner: null, nextPhase: 'progressive-showdown' };
|
return { endRound: true, winner: null, nextPhase: "progressive-showdown" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Scenario 3: All active players have acted and bets are equal ---
|
// --- Scenario 3: All active players have acted and bets are equal ---
|
||||||
const allBetsMatched = activePlayers.every(p =>
|
const allBetsMatched = activePlayers.every(
|
||||||
p.allin || // Player is all-in
|
(p) =>
|
||||||
(p.bet === room.highest_bet && p.last_played_turn === room.current_turn) // Or their bet matches the highest and they've acted this turn
|
p.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) {
|
if (allBetsMatched) {
|
||||||
let nextPhase;
|
let nextPhase;
|
||||||
switch (room.current_turn) {
|
switch (room.current_turn) {
|
||||||
case 0: nextPhase = 'flop'; break;
|
case 0:
|
||||||
case 1: nextPhase = 'turn'; break;
|
nextPhase = "flop";
|
||||||
case 2: nextPhase = 'river'; break;
|
break;
|
||||||
case 3: nextPhase = 'showdown'; break;
|
case 1:
|
||||||
default: nextPhase = null; // Should not happen
|
nextPhase = "turn";
|
||||||
}
|
break;
|
||||||
return { endRound: true, winner: null, nextPhase: nextPhase };
|
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 ---
|
// --- Default: The round continues ---
|
||||||
return { endRound: false, winner: null, nextPhase: null };
|
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.
|
* @returns {Array<string>} An array of winner IDs. Can contain multiple IDs in case of a split pot.
|
||||||
*/
|
*/
|
||||||
export function checkRoomWinners(room) {
|
export function checkRoomWinners(room) {
|
||||||
const communityCards = room.tapis;
|
const communityCards = room.tapis;
|
||||||
const activePlayers = Object.values(room.players).filter(p => !p.folded);
|
const activePlayers = Object.values(room.players).filter((p) => !p.folded);
|
||||||
|
|
||||||
// Solve each player's hand to find the best possible 5-card combination
|
// Solve each player's hand to find the best possible 5-card combination
|
||||||
const playerSolutions = activePlayers.map(player => ({
|
const playerSolutions = activePlayers.map((player) => ({
|
||||||
id: player.id,
|
id: player.id,
|
||||||
solution: Hand.solve([...communityCards, ...player.hand]),
|
solution: Hand.solve([...communityCards, ...player.hand]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (playerSolutions.length === 0) return [];
|
if (playerSolutions.length === 0) return [];
|
||||||
|
|
||||||
// Use pokersolver's `Hand.winners()` to find the best hand(s)
|
// Use pokersolver's `Hand.winners()` to find the best hand(s)
|
||||||
const winningSolutions = Hand.winners(playerSolutions.map(ps => ps.solution));
|
const winningSolutions = Hand.winners(playerSolutions.map((ps) => ps.solution));
|
||||||
|
|
||||||
// Find the player IDs that correspond to the winning hand solutions
|
// Find the player IDs that correspond to the winning hand solutions
|
||||||
const winnerIds = [];
|
const winnerIds = [];
|
||||||
for (const winningHand of winningSolutions) {
|
for (const winningHand of winningSolutions) {
|
||||||
for (const playerSol of playerSolutions) {
|
for (const playerSol of playerSolutions) {
|
||||||
// Compare description and card pool to uniquely identify the hand
|
// Compare description and card pool to uniquely identify the hand
|
||||||
if (playerSol.solution.descr === winningHand.descr && playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()) {
|
if (
|
||||||
if (!winnerIds.includes(playerSol.id)) {
|
playerSol.solution.descr === winningHand.descr &&
|
||||||
winnerIds.push(playerSol.id);
|
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 ---
|
// --- Constants for Deck Creation ---
|
||||||
import {sleep} from "openai/core";
|
import { sleep } from "openai/core";
|
||||||
import {emitSolitaireUpdate, emitUpdate} from "../server/socket.js";
|
import { emitSolitaireUpdate, emitUpdate } from "../server/socket.js";
|
||||||
|
|
||||||
const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs
|
const SUITS = ["h", "d", "s", "c"]; // Hearts, Diamonds, Spades, Clubs
|
||||||
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
|
const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"];
|
||||||
|
|
||||||
// --- Helper Functions for Card Logic ---
|
// --- Helper Functions for Card Logic ---
|
||||||
|
|
||||||
@@ -13,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).
|
* @returns {number} The numeric value (Ace=1, King=13).
|
||||||
*/
|
*/
|
||||||
function getRankValue(rank) {
|
function getRankValue(rank) {
|
||||||
if (rank === 'A') return 1;
|
if (rank === "A") return 1;
|
||||||
if (rank === 'T') return 10;
|
if (rank === "T") return 10;
|
||||||
if (rank === 'J') return 11;
|
if (rank === "J") return 11;
|
||||||
if (rank === 'Q') return 12;
|
if (rank === "Q") return 12;
|
||||||
if (rank === 'K') return 13;
|
if (rank === "K") return 13;
|
||||||
return parseInt(rank, 10);
|
return parseInt(rank, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,10 +27,9 @@ function getRankValue(rank) {
|
|||||||
* @returns {string} 'red' or 'black'.
|
* @returns {string} 'red' or 'black'.
|
||||||
*/
|
*/
|
||||||
function getCardColor(suit) {
|
function getCardColor(suit) {
|
||||||
return (suit === 'h' || suit === 'd') ? 'red' : 'black';
|
return suit === "h" || suit === "d" ? "red" : "black";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Core Game Logic Functions ---
|
// --- Core Game Logic Functions ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,13 +37,13 @@ function getCardColor(suit) {
|
|||||||
* @returns {Array<Object>} The unshuffled deck of cards.
|
* @returns {Array<Object>} The unshuffled deck of cards.
|
||||||
*/
|
*/
|
||||||
export function createDeck() {
|
export function createDeck() {
|
||||||
const deck = [];
|
const deck = [];
|
||||||
for (const suit of SUITS) {
|
for (const suit of SUITS) {
|
||||||
for (const rank of RANKS) {
|
for (const rank of RANKS) {
|
||||||
deck.push({ suit, rank, faceUp: false });
|
deck.push({ suit, rank, faceUp: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deck;
|
return deck;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,16 +52,16 @@ export function createDeck() {
|
|||||||
* @returns {Array} The shuffled array (mutated in place).
|
* @returns {Array} The shuffled array (mutated in place).
|
||||||
*/
|
*/
|
||||||
export function shuffle(array) {
|
export function shuffle(array) {
|
||||||
let currentIndex = array.length;
|
let currentIndex = array.length;
|
||||||
// While there remain elements to shuffle.
|
// While there remain elements to shuffle.
|
||||||
while (currentIndex !== 0) {
|
while (currentIndex !== 0) {
|
||||||
// Pick a remaining element.
|
// Pick a remaining element.
|
||||||
const randomIndex = Math.floor(Math.random() * currentIndex);
|
const randomIndex = Math.floor(Math.random() * currentIndex);
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
// And swap it with the current element.
|
// And swap it with the current element.
|
||||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,12 +70,12 @@ export function shuffle(array) {
|
|||||||
* @returns {function} A function that returns a pseudorandom number between 0 and 1.
|
* @returns {function} A function that returns a pseudorandom number between 0 and 1.
|
||||||
*/
|
*/
|
||||||
export function createSeededRNG(seed) {
|
export function createSeededRNG(seed) {
|
||||||
return function() {
|
return function () {
|
||||||
let t = seed += 0x6D2B79F5;
|
let t = (seed += 0x6d2b79f5);
|
||||||
t = Math.imul(t ^ t >>> 15, t | 1);
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,16 +85,16 @@ export function createSeededRNG(seed) {
|
|||||||
* @returns {Array} The shuffled array (mutated in place).
|
* @returns {Array} The shuffled array (mutated in place).
|
||||||
*/
|
*/
|
||||||
export function seededShuffle(array, rng) {
|
export function seededShuffle(array, rng) {
|
||||||
let currentIndex = array.length;
|
let currentIndex = array.length;
|
||||||
// While there remain elements to shuffle.
|
// While there remain elements to shuffle.
|
||||||
while (currentIndex !== 0) {
|
while (currentIndex !== 0) {
|
||||||
// Pick a remaining element using the seeded RNG.
|
// Pick a remaining element using the seeded RNG.
|
||||||
const randomIndex = Math.floor(rng() * currentIndex);
|
const randomIndex = Math.floor(rng() * currentIndex);
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
// And swap it with the current element.
|
// And swap it with the current element.
|
||||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||||
}
|
}
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,31 +103,31 @@ export function seededShuffle(array, rng) {
|
|||||||
* @returns {Object} The initial gameState object for Klondike Solitaire.
|
* @returns {Object} The initial gameState object for Klondike Solitaire.
|
||||||
*/
|
*/
|
||||||
export function deal(deck) {
|
export function deal(deck) {
|
||||||
const gameState = {
|
const gameState = {
|
||||||
tableauPiles: [[], [], [], [], [], [], []],
|
tableauPiles: [[], [], [], [], [], [], []],
|
||||||
foundationPiles: [[], [], [], []],
|
foundationPiles: [[], [], [], []],
|
||||||
stockPile: [],
|
stockPile: [],
|
||||||
wastePile: [],
|
wastePile: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deal cards to the 7 tableau piles
|
// Deal cards to the 7 tableau piles
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
for (let j = i; j < 7; j++) {
|
for (let j = i; j < 7; j++) {
|
||||||
gameState.tableauPiles[j].push(deck.shift());
|
gameState.tableauPiles[j].push(deck.shift());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flip the top card of each tableau pile
|
// Flip the top card of each tableau pile
|
||||||
gameState.tableauPiles.forEach(pile => {
|
gameState.tableauPiles.forEach((pile) => {
|
||||||
if (pile.length > 0) {
|
if (pile.length > 0) {
|
||||||
pile[pile.length - 1].faceUp = true;
|
pile[pile.length - 1].faceUp = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The rest of the deck becomes the stock
|
// The rest of the deck becomes the stock
|
||||||
gameState.stockPile = deck;
|
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.
|
* @returns {boolean} True if the move is valid, false otherwise.
|
||||||
*/
|
*/
|
||||||
export function isValidMove(gameState, moveData) {
|
export function isValidMove(gameState, moveData) {
|
||||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||||
|
|
||||||
// --- Get Source Pile and Card ---
|
// --- Get Source Pile and Card ---
|
||||||
let sourcePile;
|
let sourcePile;
|
||||||
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
|
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||||
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
|
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
|
||||||
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
|
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||||
else return false; // Invalid source type
|
else return false; // Invalid source type
|
||||||
|
|
||||||
const sourceCard = sourcePile?.[sourceCardIndex];
|
const sourceCard = sourcePile?.[sourceCardIndex];
|
||||||
if (!sourceCard || !sourceCard.faceUp) {
|
if (!sourceCard || !sourceCard.faceUp) {
|
||||||
return false; // Cannot move a card that doesn't exist or is face-down
|
return false; // Cannot move a card that doesn't exist or is face-down
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Validate Move TO a Tableau Pile ---
|
// --- Validate Move TO a Tableau Pile ---
|
||||||
if (destPileType === 'tableauPiles') {
|
if (destPileType === "tableauPiles") {
|
||||||
const destinationPile = gameState.tableauPiles[destPileIndex];
|
const destinationPile = gameState.tableauPiles[destPileIndex];
|
||||||
const topCard = destinationPile[destinationPile.length - 1];
|
const topCard = destinationPile[destinationPile.length - 1];
|
||||||
|
|
||||||
if (!topCard) {
|
if (!topCard) {
|
||||||
// If the destination tableau is empty, only a King can be moved there.
|
// If the destination tableau is empty, only a King can be moved there.
|
||||||
return sourceCard.rank === 'K';
|
return sourceCard.rank === "K";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card must be opposite color and one rank lower than the destination top card.
|
// Card must be opposite color and one rank lower than the destination top card.
|
||||||
const sourceColor = getCardColor(sourceCard.suit);
|
const sourceColor = getCardColor(sourceCard.suit);
|
||||||
const destColor = getCardColor(topCard.suit);
|
const destColor = getCardColor(topCard.suit);
|
||||||
const sourceValue = getRankValue(sourceCard.rank);
|
const sourceValue = getRankValue(sourceCard.rank);
|
||||||
const destValue = getRankValue(topCard.rank);
|
const destValue = getRankValue(topCard.rank);
|
||||||
return sourceColor !== destColor && destValue - sourceValue === 1;
|
return sourceColor !== destColor && destValue - sourceValue === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Validate Move TO a Foundation Pile ---
|
// --- Validate Move TO a Foundation Pile ---
|
||||||
if (destPileType === 'foundationPiles') {
|
if (destPileType === "foundationPiles") {
|
||||||
// You can only move one card at a time to a foundation pile.
|
// You can only move one card at a time to a foundation pile.
|
||||||
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
|
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
|
||||||
if (stackBeingMoved.length > 1) return false;
|
if (stackBeingMoved.length > 1) return false;
|
||||||
|
|
||||||
const destinationPile = gameState.foundationPiles[destPileIndex];
|
const destinationPile = gameState.foundationPiles[destPileIndex];
|
||||||
const topCard = destinationPile[destinationPile.length - 1];
|
const topCard = destinationPile[destinationPile.length - 1];
|
||||||
|
|
||||||
if (!topCard) {
|
if (!topCard) {
|
||||||
// If the foundation is empty, only an Ace of any suit can be moved there.
|
// If the foundation is empty, only an Ace of any suit can be moved there.
|
||||||
return sourceCard.rank === 'A';
|
return sourceCard.rank === "A";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card must be the same suit and one rank higher.
|
// Card must be the same suit and one rank higher.
|
||||||
const sourceValue = getRankValue(sourceCard.rank);
|
const sourceValue = getRankValue(sourceCard.rank);
|
||||||
const destValue = getRankValue(topCard.rank);
|
const destValue = getRankValue(topCard.rank);
|
||||||
return sourceCard.suit === topCard.suit && sourceValue - destValue === 1;
|
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.
|
* @param {Object} moveData - The details of the move.
|
||||||
*/
|
*/
|
||||||
export function moveCard(gameState, moveData) {
|
export function moveCard(gameState, moveData) {
|
||||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||||
|
|
||||||
let sourcePile;
|
let sourcePile;
|
||||||
if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex];
|
if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||||
else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile;
|
else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile;
|
||||||
else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex];
|
else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||||
|
|
||||||
let destPile;
|
let destPile;
|
||||||
if (destPileType === 'tableauPiles') destPile = gameState.tableauPiles[destPileIndex];
|
if (destPileType === "tableauPiles") destPile = gameState.tableauPiles[destPileIndex];
|
||||||
else if (destPileType === 'foundationPiles') destPile = gameState.foundationPiles[destPileIndex];
|
else if (destPileType === "foundationPiles") destPile = gameState.foundationPiles[destPileIndex];
|
||||||
|
|
||||||
// Cut the entire stack of cards to be moved from the source pile.
|
// Cut the entire stack of cards to be moved from the source pile.
|
||||||
const cardsToMove = sourcePile.splice(sourceCardIndex);
|
const cardsToMove = sourcePile.splice(sourceCardIndex);
|
||||||
// Add the stack to the destination pile.
|
// Add the stack to the destination pile.
|
||||||
destPile.push(...cardsToMove);
|
destPile.push(...cardsToMove);
|
||||||
|
|
||||||
const histMove = {
|
const histMove = {
|
||||||
move: 'move',
|
move: "move",
|
||||||
sourcePileType: sourcePileType,
|
sourcePileType: sourcePileType,
|
||||||
sourcePileIndex: sourcePileIndex,
|
sourcePileIndex: sourcePileIndex,
|
||||||
sourceCardIndex: sourceCardIndex,
|
sourceCardIndex: sourceCardIndex,
|
||||||
destPileType: destPileType,
|
destPileType: destPileType,
|
||||||
destPileIndex: destPileIndex,
|
destPileIndex: destPileIndex,
|
||||||
cardsMoved: cardsToMove,
|
cardsMoved: cardsToMove,
|
||||||
cardWasFlipped: false,
|
cardWasFlipped: false,
|
||||||
points: destPileType === 'foundationPiles' ? 11 : 1 // Points for moving to foundation
|
points: destPileType === "foundationPiles" ? 11 : 1, // Points for moving to foundation
|
||||||
}
|
};
|
||||||
|
|
||||||
// If the source was a tableau pile and there are cards left, flip the new top card.
|
// If the source was a tableau pile and there are cards left, flip the new top card.
|
||||||
if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) {
|
if (sourcePileType === "tableauPiles" && sourcePile.length > 0) {
|
||||||
sourcePile[sourcePile.length - 1].faceUp = true;
|
sourcePile[sourcePile.length - 1].faceUp = true;
|
||||||
histMove.cardWasFlipped = true;
|
histMove.cardWasFlipped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameState.hist.push(histMove)
|
gameState.hist.push(histMove);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,52 +240,51 @@ export function moveCard(gameState, moveData) {
|
|||||||
* @param {Object} gameState - The current state of the game.
|
* @param {Object} gameState - The current state of the game.
|
||||||
*/
|
*/
|
||||||
export function drawCard(gameState) {
|
export function drawCard(gameState) {
|
||||||
if (gameState.stockPile.length > 0) {
|
if (gameState.stockPile.length > 0) {
|
||||||
const card = gameState.stockPile.pop();
|
const card = gameState.stockPile.pop();
|
||||||
card.faceUp = true;
|
card.faceUp = true;
|
||||||
gameState.wastePile.push(card);
|
gameState.wastePile.push(card);
|
||||||
gameState.hist.push({
|
gameState.hist.push({
|
||||||
move: 'draw',
|
move: "draw",
|
||||||
card: card
|
card: card,
|
||||||
})
|
});
|
||||||
} else if (gameState.wastePile.length > 0) {
|
} else if (gameState.wastePile.length > 0) {
|
||||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||||
gameState.stockPile = gameState.wastePile.reverse();
|
gameState.stockPile = gameState.wastePile.reverse();
|
||||||
gameState.stockPile.forEach(card => (card.faceUp = false));
|
gameState.stockPile.forEach((card) => (card.faceUp = false));
|
||||||
gameState.wastePile = [];
|
gameState.wastePile = [];
|
||||||
gameState.hist.push({
|
gameState.hist.push({
|
||||||
move: 'draw-reset',
|
move: "draw-reset",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function draw3Cards(gameState) {
|
export function draw3Cards(gameState) {
|
||||||
if (gameState.stockPile.length > 0) {
|
if (gameState.stockPile.length > 0) {
|
||||||
let cards = []
|
let cards = [];
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
if (gameState.stockPile.length > 0) {
|
if (gameState.stockPile.length > 0) {
|
||||||
const card = gameState.stockPile.pop();
|
const card = gameState.stockPile.pop();
|
||||||
card.faceUp = true;
|
card.faceUp = true;
|
||||||
gameState.wastePile.push(card);
|
gameState.wastePile.push(card);
|
||||||
cards.push(card);
|
cards.push(card);
|
||||||
} else {
|
} else {
|
||||||
break; // Stop if stock runs out
|
break; // Stop if stock runs out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gameState.hist.push({
|
gameState.hist.push({
|
||||||
move: 'draw-3',
|
move: "draw-3",
|
||||||
cards: cards,
|
cards: cards,
|
||||||
})
|
});
|
||||||
} else if (gameState.wastePile.length > 0) {
|
} else if (gameState.wastePile.length > 0) {
|
||||||
// When stock is empty, move the entire waste pile back to stock, face down.
|
// When stock is empty, move the entire waste pile back to stock, face down.
|
||||||
gameState.stockPile = gameState.wastePile.reverse();
|
gameState.stockPile = gameState.wastePile.reverse();
|
||||||
gameState.stockPile.forEach(card => (card.faceUp = false));
|
gameState.stockPile.forEach((card) => (card.faceUp = false));
|
||||||
gameState.wastePile = [];
|
gameState.wastePile = [];
|
||||||
gameState.hist.push({
|
gameState.hist.push({
|
||||||
move: 'draw-reset',
|
move: "draw-reset",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,8 +293,8 @@ export function draw3Cards(gameState) {
|
|||||||
* @returns {boolean} True if the game is won.
|
* @returns {boolean} True if the game is won.
|
||||||
*/
|
*/
|
||||||
export function checkWinCondition(gameState) {
|
export function checkWinCondition(gameState) {
|
||||||
const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0);
|
const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0);
|
||||||
return foundationCardCount === 52;
|
return foundationCardCount === 52;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,64 +303,64 @@ export function checkWinCondition(gameState) {
|
|||||||
* @returns {boolean} True if the game can be auto-solved.
|
* @returns {boolean} True if the game can be auto-solved.
|
||||||
*/
|
*/
|
||||||
export function checkAutoSolve(gameState) {
|
export function checkAutoSolve(gameState) {
|
||||||
if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false;
|
if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false;
|
||||||
for (const pile of gameState.tableauPiles) {
|
for (const pile of gameState.tableauPiles) {
|
||||||
for (const card of pile) {
|
for (const card of pile) {
|
||||||
if (!card.faceUp) return false;
|
if (!card.faceUp) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function autoSolveMoves(userId, gameState) {
|
export function autoSolveMoves(userId, gameState) {
|
||||||
const moves = [];
|
const moves = [];
|
||||||
const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles));
|
const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles));
|
||||||
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
|
const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles));
|
||||||
|
|
||||||
function canMoveToFoundation(card) {
|
function canMoveToFoundation(card) {
|
||||||
let foundationPile = foundations.find(pile => pile[pile.length - 1]?.suit === card.suit);
|
let foundationPile = foundations.find((pile) => pile[pile.length - 1]?.suit === card.suit);
|
||||||
if (!foundationPile) {
|
if (!foundationPile) {
|
||||||
foundationPile = foundations.find(pile => pile.length === 0);
|
foundationPile = foundations.find((pile) => pile.length === 0);
|
||||||
}
|
}
|
||||||
if (foundationPile.length === 0) {
|
if (foundationPile.length === 0) {
|
||||||
return card.rank === 'A'; // Only Ace can be placed on empty foundation
|
return card.rank === "A"; // Only Ace can be placed on empty foundation
|
||||||
} else {
|
} else {
|
||||||
const topCard = foundationPile[foundationPile.length - 1];
|
const topCard = foundationPile[foundationPile.length - 1];
|
||||||
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
|
return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let moved;
|
let moved;
|
||||||
do {
|
do {
|
||||||
moved = false;
|
moved = false;
|
||||||
|
|
||||||
for (let i = 0; i < tableau.length; i++) {
|
for (let i = 0; i < tableau.length; i++) {
|
||||||
const column = tableau[i];
|
const column = tableau[i];
|
||||||
if (column.length === 0) continue;
|
if (column.length === 0) continue;
|
||||||
|
|
||||||
const card = column[column.length - 1]; // Top card of the tableau column
|
const card = column[column.length - 1]; // Top card of the tableau column
|
||||||
let foundationIndex = foundations.findIndex(pile => pile[pile.length - 1]?.suit === card.suit);
|
let foundationIndex = foundations.findIndex((pile) => pile[pile.length - 1]?.suit === card.suit);
|
||||||
if (foundationIndex === -1) {
|
if (foundationIndex === -1) {
|
||||||
foundationIndex = foundations.findIndex(pile => pile.length === 0);
|
foundationIndex = foundations.findIndex((pile) => pile.length === 0);
|
||||||
}
|
}
|
||||||
if(canMoveToFoundation(card)) {
|
if (canMoveToFoundation(card)) {
|
||||||
let moveData = {
|
let moveData = {
|
||||||
destPileIndex: foundationIndex,
|
destPileIndex: foundationIndex,
|
||||||
destPileType: 'foundationPiles',
|
destPileType: "foundationPiles",
|
||||||
sourceCardIndex: column.length - 1,
|
sourceCardIndex: column.length - 1,
|
||||||
sourcePileIndex: i,
|
sourcePileIndex: i,
|
||||||
sourcePileType: 'tableauPiles',
|
sourcePileType: "tableauPiles",
|
||||||
userId: userId,
|
userId: userId,
|
||||||
}
|
};
|
||||||
tableau[i].pop()
|
tableau[i].pop();
|
||||||
foundations[foundationIndex].push(card)
|
foundations[foundationIndex].push(card);
|
||||||
//moveCard(gameState, moveData)
|
//moveCard(gameState, moveData)
|
||||||
moves.push(moveData);
|
moves.push(moveData);
|
||||||
moved = true;
|
moved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0));
|
} while (moved); //(foundations.reduce((acc, pile) => acc + pile.length, 0));
|
||||||
emitSolitaireUpdate(userId, moves)
|
emitSolitaireUpdate(userId, moves);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,98 +369,99 @@ export function autoSolveMoves(userId, gameState) {
|
|||||||
* @param {Object} gameState - The current game state, which includes a `hist` array.
|
* @param {Object} gameState - The current game state, which includes a `hist` array.
|
||||||
*/
|
*/
|
||||||
export function undoMove(gameState) {
|
export function undoMove(gameState) {
|
||||||
if (!gameState.hist || gameState.hist.length === 0) {
|
if (!gameState.hist || gameState.hist.length === 0) {
|
||||||
console.log("No moves to undo.");
|
console.log("No moves to undo.");
|
||||||
return; // Nothing to undo
|
return; // Nothing to undo
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastMove = gameState.hist.pop(); // Get and remove the last move from history
|
const lastMove = gameState.hist.pop(); // Get and remove the last move from history
|
||||||
gameState.moves++; // Undoing a move counts as a new move
|
gameState.moves++; // Undoing a move counts as a new move
|
||||||
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
|
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
|
||||||
|
|
||||||
switch (lastMove.move) {
|
switch (lastMove.move) {
|
||||||
case 'move':
|
case "move":
|
||||||
undoCardMove(gameState, lastMove);
|
undoCardMove(gameState, lastMove);
|
||||||
break;
|
break;
|
||||||
case 'draw':
|
case "draw":
|
||||||
undoDraw(gameState, lastMove);
|
undoDraw(gameState, lastMove);
|
||||||
break;
|
break;
|
||||||
case 'draw-3':
|
case "draw-3":
|
||||||
undoDraw3(gameState, lastMove);
|
undoDraw3(gameState, lastMove);
|
||||||
break;
|
break;
|
||||||
case 'draw-reset':
|
case "draw-reset":
|
||||||
undoDrawReset(gameState, lastMove);
|
undoDrawReset(gameState, lastMove);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// If an unknown move type is found, push it back to avoid corrupting the history
|
// If an unknown move type is found, push it back to avoid corrupting the history
|
||||||
gameState.hist.push(lastMove);
|
gameState.hist.push(lastMove);
|
||||||
gameState.moves--; // Revert the move count increment
|
gameState.moves--; // Revert the move count increment
|
||||||
gameState.score += lastMove.points || 1; // Revert the score decrement
|
gameState.score += lastMove.points || 1; // Revert the score decrement
|
||||||
console.error("Unknown move type in history:", lastMove);
|
console.error("Unknown move type in history:", lastMove);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper functions for undoing specific moves ---
|
// --- Helper functions for undoing specific moves ---
|
||||||
|
|
||||||
function undoCardMove(gameState, moveData) {
|
function undoCardMove(gameState, moveData) {
|
||||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = moveData;
|
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } =
|
||||||
|
moveData;
|
||||||
|
|
||||||
// 1. Find the destination pile (where the cards are NOW)
|
// 1. Find the destination pile (where the cards are NOW)
|
||||||
let currentPile;
|
let currentPile;
|
||||||
if (destPileType === 'tableauPiles') currentPile = gameState.tableauPiles[destPileIndex];
|
if (destPileType === "tableauPiles") currentPile = gameState.tableauPiles[destPileIndex];
|
||||||
else if (destPileType === 'foundationPiles') currentPile = gameState.foundationPiles[destPileIndex];
|
else if (destPileType === "foundationPiles") currentPile = gameState.foundationPiles[destPileIndex];
|
||||||
|
|
||||||
// 2. Remove the moved cards from their current pile
|
// 2. Remove the moved cards from their current pile
|
||||||
// Using splice with a negative index removes from the end of the array
|
// Using splice with a negative index removes from the end of the array
|
||||||
currentPile.splice(-cardsMoved.length);
|
currentPile.splice(-cardsMoved.length);
|
||||||
|
|
||||||
// 3. Find the original source pile
|
// 3. Find the original source pile
|
||||||
let originalPile;
|
let originalPile;
|
||||||
if (sourcePileType === 'tableauPiles') originalPile = gameState.tableauPiles[sourcePileIndex];
|
if (sourcePileType === "tableauPiles") originalPile = gameState.tableauPiles[sourcePileIndex];
|
||||||
else if (sourcePileType === 'wastePile') originalPile = gameState.wastePile;
|
else if (sourcePileType === "wastePile") originalPile = gameState.wastePile;
|
||||||
else if (sourcePileType === 'foundationPiles') originalPile = gameState.foundationPiles[sourcePileIndex];
|
else if (sourcePileType === "foundationPiles") originalPile = gameState.foundationPiles[sourcePileIndex];
|
||||||
|
|
||||||
// 4. Put the cards back where they came from
|
// 4. Put the cards back where they came from
|
||||||
// Using splice to insert the cards back at their original index
|
// Using splice to insert the cards back at their original index
|
||||||
originalPile.splice(sourceCardIndex, 0, ...cardsMoved);
|
originalPile.splice(sourceCardIndex, 0, ...cardsMoved);
|
||||||
|
|
||||||
// 5. If a card was flipped during the move, flip it back to face-down
|
// 5. If a card was flipped during the move, flip it back to face-down
|
||||||
if (cardWasFlipped) {
|
if (cardWasFlipped) {
|
||||||
const cardToUnflip = originalPile[sourceCardIndex - 1];
|
const cardToUnflip = originalPile[sourceCardIndex - 1];
|
||||||
if (cardToUnflip) {
|
if (cardToUnflip) {
|
||||||
cardToUnflip.faceUp = false;
|
cardToUnflip.faceUp = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoDraw(gameState, moveData) {
|
function undoDraw(gameState, moveData) {
|
||||||
// A 'draw' move means a card went from stock to waste.
|
// 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.
|
// To undo, move it from waste back to stock and flip it face-down.
|
||||||
const cardToReturn = gameState.wastePile.pop();
|
const cardToReturn = gameState.wastePile.pop();
|
||||||
if (cardToReturn) {
|
if (cardToReturn) {
|
||||||
cardToReturn.faceUp = false;
|
cardToReturn.faceUp = false;
|
||||||
gameState.stockPile.push(cardToReturn);
|
gameState.stockPile.push(cardToReturn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoDraw3(gameState, moveData) {
|
function undoDraw3(gameState, moveData) {
|
||||||
// A 'draw-3' move means up to 3 cards went from stock to
|
// 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.
|
// waste. To undo, move them back to stock and flip them face-down.
|
||||||
const cardsToReturn = moveData.cards || [];
|
const cardsToReturn = moveData.cards || [];
|
||||||
for (let i = 0; i < cardsToReturn.length; i++) {
|
for (let i = 0; i < cardsToReturn.length; i++) {
|
||||||
const card = gameState.wastePile.pop();
|
const card = gameState.wastePile.pop();
|
||||||
if (card) {
|
if (card) {
|
||||||
card.faceUp = false;
|
card.faceUp = false;
|
||||||
gameState.stockPile.push(card);
|
gameState.stockPile.push(card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoDrawReset(gameState, moveData) {
|
function undoDrawReset(gameState, moveData) {
|
||||||
// A 'draw-reset' means the waste pile was moved to the stock pile.
|
// A 'draw-reset' means the waste pile was moved to the stock pile.
|
||||||
// To undo, move the stock pile back to the waste pile and flip cards face-up.
|
// To undo, move the stock pile back to the waste pile and flip cards face-up.
|
||||||
gameState.wastePile = gameState.stockPile.reverse();
|
gameState.wastePile = gameState.stockPile.reverse();
|
||||||
gameState.wastePile.forEach(card => (card.faceUp = true));
|
gameState.wastePile.forEach((card) => (card.faceUp = true));
|
||||||
gameState.stockPile = [];
|
gameState.stockPile = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export let activePredis = {};
|
|||||||
// Format: { [userId]: { endAt, lastMessage } }
|
// Format: { [userId]: { endAt, lastMessage } }
|
||||||
export let activeSlowmodes = {};
|
export let activeSlowmodes = {};
|
||||||
|
|
||||||
|
|
||||||
// --- Queues for Matchmaking ---
|
// --- Queues for Matchmaking ---
|
||||||
|
|
||||||
// Stores user IDs waiting to play Tic-Tac-Toe.
|
// Stores user IDs waiting to play Tic-Tac-Toe.
|
||||||
@@ -55,7 +54,6 @@ export let connect4Queue = [];
|
|||||||
|
|
||||||
export let queueMessagesEndpoints = [];
|
export let queueMessagesEndpoints = [];
|
||||||
|
|
||||||
|
|
||||||
// --- Rate Limiting and Caching ---
|
// --- Rate Limiting and Caching ---
|
||||||
|
|
||||||
// Tracks message timestamps for the channel points system, keyed by user ID.
|
// Tracks message timestamps for the channel points system, keyed by user ID.
|
||||||
@@ -70,4 +68,4 @@ export let requestTimestamps = new Map();
|
|||||||
|
|
||||||
// In-memory cache for Valorant skin data fetched from the API.
|
// In-memory cache for Valorant skin data fetched from the API.
|
||||||
// This prevents re-fetching the same data on every command use.
|
// This prevents re-fetching the same data on every command use.
|
||||||
export let skins = [];
|
export let skins = [];
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ export const C4_COLS = 7;
|
|||||||
|
|
||||||
// A predefined list of choices for the /timeout command's duration option.
|
// A predefined list of choices for the /timeout command's duration option.
|
||||||
const TimesChoices = [
|
const TimesChoices = [
|
||||||
{ name: '1 minute', value: 60 },
|
{ name: "1 minute", value: 60 },
|
||||||
{ name: '5 minutes', value: 300 },
|
{ name: "5 minutes", value: 300 },
|
||||||
{ name: '10 minutes', value: 600 },
|
{ name: "10 minutes", value: 600 },
|
||||||
{ name: '15 minutes', value: 900 },
|
{ name: "15 minutes", value: 900 },
|
||||||
{ name: '30 minutes', value: 1800 },
|
{ name: "30 minutes", value: 1800 },
|
||||||
{ name: '1 heure', value: 3600 },
|
{ name: "1 heure", value: 3600 },
|
||||||
{ name: '2 heures', value: 7200 },
|
{ name: "2 heures", value: 7200 },
|
||||||
{ name: '3 heures', value: 10800 },
|
{ name: "3 heures", value: 10800 },
|
||||||
{ name: '6 heures', value: 21600 },
|
{ name: "6 heures", value: 21600 },
|
||||||
{ name: '9 heures', value: 32400 },
|
{ name: "9 heures", value: 32400 },
|
||||||
{ name: '12 heures', value: 43200 },
|
{ name: "12 heures", value: 43200 },
|
||||||
{ name: '16 heures', value: 57600 },
|
{ name: "16 heures", value: 57600 },
|
||||||
{ name: '1 jour', value: 86400 },
|
{ name: "1 jour", value: 86400 },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,10 +24,9 @@ const TimesChoices = [
|
|||||||
* @returns {Array<object>} The array of time choices.
|
* @returns {Array<object>} The array of time choices.
|
||||||
*/
|
*/
|
||||||
export function getTimesChoices() {
|
export function getTimesChoices() {
|
||||||
return TimesChoices;
|
return TimesChoices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Connect 4 Logic ---
|
// --- Connect 4 Logic ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +34,9 @@ export function getTimesChoices() {
|
|||||||
* @returns {Array<Array<null>>} A 2D array representing the board.
|
* @returns {Array<Array<null>>} A 2D array representing the board.
|
||||||
*/
|
*/
|
||||||
export function createConnect4Board() {
|
export function createConnect4Board() {
|
||||||
return Array(C4_ROWS).fill(null).map(() => Array(C4_COLS).fill(null));
|
return Array(C4_ROWS)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => Array(C4_COLS).fill(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,43 +46,95 @@ export function createConnect4Board() {
|
|||||||
* @returns {object} An object with `win` (boolean) and `pieces` (array of winning piece coordinates).
|
* @returns {object} An object with `win` (boolean) and `pieces` (array of winning piece coordinates).
|
||||||
*/
|
*/
|
||||||
export function checkConnect4Win(board, player) {
|
export function checkConnect4Win(board, player) {
|
||||||
// Check horizontal
|
// Check horizontal
|
||||||
for (let r = 0; r < C4_ROWS; r++) {
|
for (let r = 0; r < C4_ROWS; r++) {
|
||||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||||
if (board[r][c] === player && board[r][c+1] === player && board[r][c+2] === player && board[r][c+3] === player) {
|
if (
|
||||||
return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] };
|
board[r][c] === player &&
|
||||||
}
|
board[r][c + 1] === player &&
|
||||||
}
|
board[r][c + 2] === player &&
|
||||||
}
|
board[r][c + 3] === player
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
win: true,
|
||||||
|
pieces: [
|
||||||
|
{ row: r, col: c },
|
||||||
|
{ row: r, col: c + 1 },
|
||||||
|
{ row: r, col: c + 2 },
|
||||||
|
{ row: r, col: c + 3 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check vertical
|
// Check vertical
|
||||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||||
for (let c = 0; c < C4_COLS; c++) {
|
for (let c = 0; c < C4_COLS; c++) {
|
||||||
if (board[r][c] === player && board[r+1][c] === player && board[r+2][c] === player && board[r+3][c] === player) {
|
if (
|
||||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] };
|
board[r][c] === player &&
|
||||||
}
|
board[r + 1][c] === player &&
|
||||||
}
|
board[r + 2][c] === player &&
|
||||||
}
|
board[r + 3][c] === player
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
win: true,
|
||||||
|
pieces: [
|
||||||
|
{ row: r, col: c },
|
||||||
|
{ row: r + 1, col: c },
|
||||||
|
{ row: r + 2, col: c },
|
||||||
|
{ row: r + 3, col: c },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check diagonal (down-right)
|
// Check diagonal (down-right)
|
||||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||||
if (board[r][c] === player && board[r+1][c+1] === player && board[r+2][c+2] === player && board[r+3][c+3] === player) {
|
if (
|
||||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c+1}, {row:r+2, col:c+2}, {row:r+3, col:c+3}] };
|
board[r][c] === player &&
|
||||||
}
|
board[r + 1][c + 1] === player &&
|
||||||
}
|
board[r + 2][c + 2] === player &&
|
||||||
}
|
board[r + 3][c + 3] === player
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
win: true,
|
||||||
|
pieces: [
|
||||||
|
{ row: r, col: c },
|
||||||
|
{ row: r + 1, col: c + 1 },
|
||||||
|
{ row: r + 2, col: c + 2 },
|
||||||
|
{ row: r + 3, col: c + 3 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check diagonal (up-right)
|
// Check diagonal (up-right)
|
||||||
for (let r = 3; r < C4_ROWS; r++) {
|
for (let r = 3; r < C4_ROWS; r++) {
|
||||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||||
if (board[r][c] === player && board[r-1][c+1] === player && board[r-2][c+2] === player && board[r-3][c+3] === player) {
|
if (
|
||||||
return { win: true, pieces: [{row:r, col:c}, {row:r-1, col:c+1}, {row:r-2, col:c+2}, {row:r-3, col:c+3}] };
|
board[r][c] === player &&
|
||||||
}
|
board[r - 1][c + 1] === player &&
|
||||||
}
|
board[r - 2][c + 2] === player &&
|
||||||
}
|
board[r - 3][c + 3] === player
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
win: true,
|
||||||
|
pieces: [
|
||||||
|
{ row: r, col: c },
|
||||||
|
{ row: r - 1, col: c + 1 },
|
||||||
|
{ row: r - 2, col: c + 2 },
|
||||||
|
{ row: r - 3, col: c + 3 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
* @returns {boolean} True if the game is a draw.
|
||||||
*/
|
*/
|
||||||
export function checkConnect4Draw(board) {
|
export function checkConnect4Draw(board) {
|
||||||
// A draw occurs if the top row is completely full.
|
// A draw occurs if the top row is completely full.
|
||||||
return board[0].every(cell => cell !== null);
|
return board[0].every((cell) => cell !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,10 +153,10 @@ export function checkConnect4Draw(board) {
|
|||||||
* @returns {string} The formatted string representation of the board.
|
* @returns {string} The formatted string representation of the board.
|
||||||
*/
|
*/
|
||||||
export function formatConnect4BoardForDiscord(board) {
|
export function formatConnect4BoardForDiscord(board) {
|
||||||
const symbols = {
|
const symbols = {
|
||||||
'R': '🔴',
|
R: "🔴",
|
||||||
'Y': '🟡',
|
Y: "🟡",
|
||||||
null: '⚪' // Using a white circle for empty slots
|
null: "⚪", // Using a white circle for empty slots
|
||||||
};
|
};
|
||||||
return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n');
|
return board.map((row) => row.map((cell) => symbols[cell]).join("")).join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,65 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import express from 'express';
|
import express from "express";
|
||||||
import { verifyKeyMiddleware } from 'discord-interactions';
|
import { verifyKeyMiddleware } from "discord-interactions";
|
||||||
import { handleInteraction } from '../bot/handlers/interactionCreate.js';
|
import { handleInteraction } from "../bot/handlers/interactionCreate.js";
|
||||||
import { client } from '../bot/client.js';
|
import { client } from "../bot/client.js";
|
||||||
|
|
||||||
// Import route handlers
|
// Import route handlers
|
||||||
import { apiRoutes } from './routes/api.js';
|
import { apiRoutes } from "./routes/api.js";
|
||||||
import { pokerRoutes } from './routes/poker.js';
|
import { pokerRoutes } from "./routes/poker.js";
|
||||||
import { solitaireRoutes } from './routes/solitaire.js';
|
import { solitaireRoutes } from "./routes/solitaire.js";
|
||||||
import {getSocketIo} from "./socket.js";
|
import { getSocketIo } from "./socket.js";
|
||||||
import {erinyesRoutes} from "./routes/erinyes.js";
|
import { blackjackRoutes } from "./routes/blackjack.js";
|
||||||
import {blackjackRoutes} from "./routes/blackjack.js";
|
import { marketRoutes } from "./routes/market.js";
|
||||||
|
|
||||||
// --- EXPRESS APP INITIALIZATION ---
|
// --- EXPRESS APP INITIALIZATION ---
|
||||||
const app = express();
|
const app = express();
|
||||||
const io = getSocketIo();
|
const io = getSocketIo();
|
||||||
const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||||
|
|
||||||
// --- GLOBAL MIDDLEWARE ---
|
// --- GLOBAL MIDDLEWARE ---
|
||||||
|
|
||||||
// CORS Middleware
|
// CORS Middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', FLAPI_URL);
|
res.header("Access-Control-Allow-Origin", FLAPI_URL);
|
||||||
res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires');
|
res.header(
|
||||||
next();
|
"Access-Control-Allow-Headers",
|
||||||
|
"Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
|
||||||
|
);
|
||||||
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- PRIMARY DISCORD INTERACTION ENDPOINT ---
|
// --- PRIMARY DISCORD INTERACTION ENDPOINT ---
|
||||||
// This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.)
|
// This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.)
|
||||||
app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
|
app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => {
|
||||||
// The actual logic is delegated to a dedicated handler for better organization
|
// The actual logic is delegated to a dedicated handler for better organization
|
||||||
await handleInteraction(req, res, client);
|
await handleInteraction(req, res, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
// JSON Body Parser Middleware
|
// JSON Body Parser Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// --- STATIC ASSETS ---
|
// --- STATIC ASSETS ---
|
||||||
app.use('/public', express.static('public'));
|
app.use("/public", express.static("public"));
|
||||||
|
|
||||||
|
|
||||||
// --- API ROUTES ---
|
// --- API ROUTES ---
|
||||||
|
|
||||||
// General API routes (users, polls, etc.)
|
// General API routes (users, polls, etc.)
|
||||||
app.use('/api', apiRoutes(client, io));
|
app.use("/api", apiRoutes(client, io));
|
||||||
|
|
||||||
// Poker-specific routes
|
// Poker-specific routes
|
||||||
app.use('/api/poker', pokerRoutes(client, io));
|
app.use("/api/poker", pokerRoutes(client, io));
|
||||||
|
|
||||||
// Solitaire-specific routes
|
// Solitaire-specific routes
|
||||||
app.use('/api/solitaire', solitaireRoutes(client, io));
|
app.use("/api/solitaire", solitaireRoutes(client, io));
|
||||||
|
|
||||||
app.use('/api/blackjack', blackjackRoutes(client, io));
|
// Blackjack-specific routes
|
||||||
|
app.use("/api/blackjack", blackjackRoutes(client, io));
|
||||||
|
|
||||||
|
// Market-specific routes
|
||||||
|
app.use("/api/market-place", marketRoutes(client, io));
|
||||||
|
|
||||||
// erinyes-specific routes
|
// erinyes-specific routes
|
||||||
app.use('/api/erinyes', erinyesRoutes(client, io));
|
// app.use("/api/erinyes", erinyesRoutes(client, io));
|
||||||
|
|
||||||
|
export { app };
|
||||||
export { app };
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,342 +1,364 @@
|
|||||||
// /routes/blackjack.js
|
// /routes/blackjack.js
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {
|
import {
|
||||||
createBlackjackRoom,
|
createBlackjackRoom,
|
||||||
startBetting,
|
startBetting,
|
||||||
dealInitial,
|
dealInitial,
|
||||||
autoActions,
|
autoActions,
|
||||||
everyoneDone,
|
everyoneDone,
|
||||||
dealerPlay,
|
dealerPlay,
|
||||||
settleAll,
|
settleAll,
|
||||||
applyAction,
|
applyAction,
|
||||||
publicPlayerView,
|
publicPlayerView,
|
||||||
handValue,
|
handValue,
|
||||||
dealerShouldHit, draw
|
dealerShouldHit,
|
||||||
|
draw,
|
||||||
} from "../../game/blackjack.js";
|
} from "../../game/blackjack.js";
|
||||||
|
|
||||||
// Optional: hook into your DB & Discord systems if available
|
// Optional: hook into your DB & Discord systems if available
|
||||||
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
||||||
import { client } from "../../bot/client.js";
|
import { client } from "../../bot/client.js";
|
||||||
import {emitToast, emitUpdate} from "../socket.js";
|
import { emitToast, emitUpdate } from "../socket.js";
|
||||||
import {EmbedBuilder} from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
|
|
||||||
export function blackjackRoutes(io) {
|
export function blackjackRoutes(io) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// --- Singleton continuous room ---
|
// --- Singleton continuous room ---
|
||||||
const room = createBlackjackRoom({
|
const room = createBlackjackRoom({
|
||||||
minBet: 10,
|
minBet: 10,
|
||||||
maxBet: 10000,
|
maxBet: 10000,
|
||||||
fakeMoney: false,
|
fakeMoney: false,
|
||||||
decks: 6,
|
decks: 6,
|
||||||
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
hitSoft17: false, // S17 (dealer stands on soft 17) if false
|
||||||
blackjackPayout: 1.5, // 3:2
|
blackjackPayout: 1.5, // 3:2
|
||||||
cutCardRatio: 0.25,
|
cutCardRatio: 0.25,
|
||||||
phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 7000 },
|
phaseDurations: {
|
||||||
animation: { dealerDrawMs: 1000 }
|
bettingMs: 10000,
|
||||||
});
|
dealMs: 2000,
|
||||||
|
playMsPerPlayer: 20000,
|
||||||
|
revealMs: 1000,
|
||||||
|
payoutMs: 7000,
|
||||||
|
},
|
||||||
|
animation: { dealerDrawMs: 1000 },
|
||||||
|
});
|
||||||
|
|
||||||
const sleep = (ms) => new Promise(res => setTimeout(res, ms));
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||||
let animatingDealer = false;
|
let animatingDealer = false;
|
||||||
|
|
||||||
async function runDealerAnimation() {
|
async function runDealerAnimation() {
|
||||||
if (animatingDealer) return;
|
if (animatingDealer) return;
|
||||||
animatingDealer = true;
|
animatingDealer = true;
|
||||||
|
|
||||||
room.status = "dealer";
|
room.status = "dealer";
|
||||||
room.dealer.holeHidden = false;
|
room.dealer.holeHidden = false;
|
||||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000);
|
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000);
|
||||||
emitUpdate("dealer-reveal", snapshot(room));
|
emitUpdate("dealer-reveal", snapshot(room));
|
||||||
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
await sleep(room.settings.phaseDurations.revealMs ?? 1000);
|
||||||
|
|
||||||
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
|
||||||
room.dealer.cards.push(draw(room.shoe));
|
room.dealer.cards.push(draw(room.shoe));
|
||||||
room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500);
|
room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500);
|
||||||
emitUpdate("dealer-hit", snapshot(room));
|
emitUpdate("dealer-hit", snapshot(room));
|
||||||
await sleep(room.settings.animation?.dealerDrawMs ?? 500);
|
await sleep(room.settings.animation?.dealerDrawMs ?? 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
settleAll(room);
|
settleAll(room);
|
||||||
room.status = "payout";
|
room.status = "payout";
|
||||||
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
|
room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000);
|
||||||
emitUpdate("payout", snapshot(room))
|
emitUpdate("payout", snapshot(room));
|
||||||
|
|
||||||
animatingDealer = false;
|
animatingDealer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoTimeoutAFK(now) {
|
function autoTimeoutAFK(now) {
|
||||||
if (room.status !== "playing") return false;
|
if (room.status !== "playing") return false;
|
||||||
if (!room.phase_ends_at || now < room.phase_ends_at) return false;
|
if (!room.phase_ends_at || now < room.phase_ends_at) return false;
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
for (const p of Object.values(room.players)) {
|
for (const p of Object.values(room.players)) {
|
||||||
if (!p.inRound) continue;
|
if (!p.inRound) continue;
|
||||||
const h = p.hands[p.activeHand];
|
const h = p.hands[p.activeHand];
|
||||||
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
||||||
h.surrendered = true;
|
h.surrendered = true;
|
||||||
h.stood = true;
|
h.stood = true;
|
||||||
h.hasActed = true;
|
h.hasActed = true;
|
||||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||||
emitToast({ type: "player-timeout", userId: p.id });
|
emitToast({ type: "player-timeout", userId: p.id });
|
||||||
changed = true;
|
changed = true;
|
||||||
} else if (h.hasActed && !h.stood) {
|
} else if (h.hasActed && !h.stood) {
|
||||||
h.stood = true;
|
h.stood = true;
|
||||||
room.leavingAfterRound[p.id] = true; // kick at end of round
|
room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshot(r) {
|
function snapshot(r) {
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
phase_ends_at: r.phase_ends_at,
|
phase_ends_at: r.phase_ends_at,
|
||||||
minBet: r.minBet,
|
minBet: r.minBet,
|
||||||
maxBet: r.maxBet,
|
maxBet: r.maxBet,
|
||||||
settings: r.settings,
|
settings: r.settings,
|
||||||
dealer: { cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total },
|
dealer: {
|
||||||
players: Object.values(r.players).map(publicPlayerView),
|
cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards,
|
||||||
shoeCount: r.shoe.length,
|
total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total,
|
||||||
};
|
},
|
||||||
}
|
players: Object.values(r.players).map(publicPlayerView),
|
||||||
|
shoeCount: r.shoe.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --- Public endpoints ---
|
// --- Public endpoints ---
|
||||||
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
|
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
|
||||||
|
|
||||||
router.post("/join", async (req, res) => {
|
router.post("/join", async (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
if (!userId) return res.status(400).json({ message: "userId required" });
|
if (!userId) return res.status(400).json({ message: "userId required" });
|
||||||
if (room.players[userId]) return res.status(200).json({ message: "Already here" });
|
if (room.players[userId]) return res.status(200).json({ message: "Already here" });
|
||||||
|
|
||||||
const user = await client.users.fetch(userId);
|
const user = await client.users.fetch(userId);
|
||||||
const bank = getUser.get(userId)?.coins ?? 0;
|
const bank = getUser.get(userId)?.coins ?? 0;
|
||||||
|
|
||||||
room.players[userId] = {
|
room.players[userId] = {
|
||||||
id: userId,
|
id: userId,
|
||||||
globalName: user.globalName || user.username,
|
globalName: user.globalName || user.username,
|
||||||
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
bank,
|
bank,
|
||||||
currentBet: 0,
|
currentBet: 0,
|
||||||
inRound: false,
|
inRound: false,
|
||||||
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 }],
|
hands: [
|
||||||
activeHand: 0,
|
{
|
||||||
joined_at: Date.now(),
|
cards: [],
|
||||||
msgId: null,
|
stood: false,
|
||||||
totalDelta: 0,
|
busted: false,
|
||||||
totalBets: 0,
|
doubled: false,
|
||||||
};
|
surrendered: false,
|
||||||
|
hasActed: false,
|
||||||
|
bet: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeHand: 0,
|
||||||
|
joined_at: Date.now(),
|
||||||
|
msgId: null,
|
||||||
|
totalDelta: 0,
|
||||||
|
totalBets: 0,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||||
const generalChannel = guild.channels.cache.find(
|
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||||
ch => ch.name === 'général' || ch.name === 'general'
|
const embed = new EmbedBuilder()
|
||||||
);
|
.setDescription(`<@${userId}> joue au Blackjack`)
|
||||||
const embed = new EmbedBuilder()
|
.addFields(
|
||||||
.setDescription(`<@${userId}> joue au Blackjack`)
|
{
|
||||||
.addFields(
|
name: `Gains`,
|
||||||
{
|
value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
||||||
name: `Gains`,
|
inline: true,
|
||||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
},
|
||||||
inline: true
|
{
|
||||||
},
|
name: `Mises jouées`,
|
||||||
{
|
value: `**${room.players[userId].totalBets}**`,
|
||||||
name: `Mises jouées`,
|
inline: true,
|
||||||
value: `**${room.players[userId].totalBets}**`,
|
},
|
||||||
inline: true
|
)
|
||||||
}
|
.setColor("#5865f2")
|
||||||
)
|
.setTimestamp(new Date());
|
||||||
.setColor('#5865f2')
|
|
||||||
.setTimestamp(new Date());
|
|
||||||
|
|
||||||
const msg = await generalChannel.send({ embeds: [embed] });
|
const msg = await generalChannel.send({ embeds: [embed] });
|
||||||
room.players[userId].msgId = msg.id;
|
room.players[userId].msgId = msg.id;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitUpdate("player-joined", snapshot(room));
|
emitUpdate("player-joined", snapshot(room));
|
||||||
return res.status(200).json({ message: "joined" });
|
return res.status(200).json({ message: "joined" });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/leave", async (req, res) => {
|
router.post("/leave", async (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" });
|
if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||||
const generalChannel = guild.channels.cache.find(
|
const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general");
|
||||||
ch => ch.name === 'général' || ch.name === 'general'
|
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
|
||||||
);
|
const updatedEmbed = new EmbedBuilder()
|
||||||
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
|
.setDescription(`<@${userId}> a quitté la table de Blackjack.`)
|
||||||
const updatedEmbed = new EmbedBuilder()
|
.addFields(
|
||||||
.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`,
|
||||||
name: `Gains`,
|
inline: true,
|
||||||
value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`,
|
},
|
||||||
inline: true
|
{
|
||||||
},
|
name: `Mises jouées`,
|
||||||
{
|
value: `**${room.players[userId].totalBets}**`,
|
||||||
name: `Mises jouées`,
|
inline: true,
|
||||||
value: `**${room.players[userId].totalBets}**`,
|
},
|
||||||
inline: true
|
)
|
||||||
}
|
.setColor(room.players[userId].totalDelta >= 0 ? 0x22a55b : 0xed4245)
|
||||||
)
|
.setTimestamp(new Date());
|
||||||
.setColor(room.players[userId].totalDelta >= 0 ? 0x22A55B : 0xED4245)
|
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
||||||
.setTimestamp(new Date());
|
} catch (e) {
|
||||||
await msg.edit({ embeds: [updatedEmbed], components: [] });
|
console.log(e);
|
||||||
} catch (e) {
|
}
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = room.players[userId];
|
const p = room.players[userId];
|
||||||
if (p.inRound) {
|
if (p.inRound) {
|
||||||
// leave after round to avoid abandoning an active bet
|
// leave after round to avoid abandoning an active bet
|
||||||
room.leavingAfterRound[userId] = true;
|
room.leavingAfterRound[userId] = true;
|
||||||
return res.status(200).json({ message: "will-leave-after-round" });
|
return res.status(200).json({ message: "will-leave-after-round" });
|
||||||
} else {
|
} else {
|
||||||
delete room.players[userId];
|
delete room.players[userId];
|
||||||
emitUpdate("player-left", snapshot(room));
|
emitUpdate("player-left", snapshot(room));
|
||||||
return res.status(200).json({ message: "left" });
|
return res.status(200).json({ message: "left" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/bet", (req, res) => {
|
router.post("/bet", (req, res) => {
|
||||||
const { userId, amount } = req.body;
|
const { userId, amount } = req.body;
|
||||||
const p = room.players[userId];
|
const p = room.players[userId];
|
||||||
if (!p) return res.status(404).json({ message: "not in room" });
|
if (!p) return res.status(404).json({ message: "not in room" });
|
||||||
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
|
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
|
||||||
|
|
||||||
const bet = Math.floor(Number(amount) || 0);
|
const bet = Math.floor(Number(amount) || 0);
|
||||||
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
|
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
|
||||||
|
|
||||||
if (!room.settings.fakeMoney) {
|
if (!room.settings.fakeMoney) {
|
||||||
const userDB = getUser.get(userId);
|
const userDB = getUser.get(userId);
|
||||||
const coins = userDB?.coins ?? 0;
|
const coins = userDB?.coins ?? 0;
|
||||||
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
|
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
|
||||||
updateUserCoins.run({ id: userId, coins: coins - bet });
|
updateUserCoins.run({ id: userId, coins: coins - bet });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-blackjack-${Date.now()}`,
|
id: `${userId}-blackjack-${Date.now()}`,
|
||||||
user_id: userId, target_user_id: null,
|
user_id: userId,
|
||||||
action: 'BLACKJACK_BET',
|
target_user_id: null,
|
||||||
coins_amount: -bet, user_new_amount: coins - bet,
|
action: "BLACKJACK_BET",
|
||||||
});
|
coins_amount: -bet,
|
||||||
p.bank = coins - bet;
|
user_new_amount: coins - bet,
|
||||||
}
|
});
|
||||||
|
p.bank = coins - bet;
|
||||||
|
}
|
||||||
|
|
||||||
p.currentBet = bet;
|
p.currentBet = bet;
|
||||||
p.hands[p.activeHand].bet = bet;
|
p.hands[p.activeHand].bet = bet;
|
||||||
emitToast({ type: "player-bet", userId, amount: bet });
|
emitToast({ type: "player-bet", userId, amount: bet });
|
||||||
emitUpdate("bet-placed", snapshot(room));
|
emitUpdate("bet-placed", snapshot(room));
|
||||||
return res.status(200).json({ message: "bet-accepted" });
|
return res.status(200).json({ message: "bet-accepted" });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/action/:action", (req, res) => {
|
router.post("/action/:action", (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
const action = req.params.action;
|
const action = req.params.action;
|
||||||
const p = room.players[userId];
|
const p = room.players[userId];
|
||||||
if (!p) return res.status(404).json({ message: "not in room" });
|
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" });
|
if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" });
|
||||||
|
|
||||||
// Handle extra coin lock for double
|
// Handle extra coin lock for double
|
||||||
if (action === "double" && !room.settings.fakeMoney) {
|
if (action === "double" && !room.settings.fakeMoney) {
|
||||||
const userDB = getUser.get(userId);
|
const userDB = getUser.get(userId);
|
||||||
const coins = userDB?.coins ?? 0;
|
const coins = userDB?.coins ?? 0;
|
||||||
const hand = p.hands[p.activeHand];
|
const hand = p.hands[p.activeHand];
|
||||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
|
||||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-blackjack-${Date.now()}`,
|
id: `${userId}-blackjack-${Date.now()}`,
|
||||||
user_id: userId, target_user_id: null,
|
user_id: userId,
|
||||||
action: 'BLACKJACK_DOUBLE',
|
target_user_id: null,
|
||||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
action: "BLACKJACK_DOUBLE",
|
||||||
});
|
coins_amount: -hand.bet,
|
||||||
p.bank = coins - hand.bet;
|
user_new_amount: coins - hand.bet,
|
||||||
// effective bet size is handled in settlement via hand.doubled flag
|
});
|
||||||
}
|
p.bank = coins - hand.bet;
|
||||||
|
// effective bet size is handled in settlement via hand.doubled flag
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "split" && !room.settings.fakeMoney) {
|
if (action === "split" && !room.settings.fakeMoney) {
|
||||||
const userDB = getUser.get(userId);
|
const userDB = getUser.get(userId);
|
||||||
const coins = userDB?.coins ?? 0;
|
const coins = userDB?.coins ?? 0;
|
||||||
const hand = p.hands[p.activeHand];
|
const hand = p.hands[p.activeHand];
|
||||||
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
|
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
|
||||||
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-blackjack-${Date.now()}`,
|
id: `${userId}-blackjack-${Date.now()}`,
|
||||||
user_id: userId, target_user_id: null,
|
user_id: userId,
|
||||||
action: 'BLACKJACK_SPLIT',
|
target_user_id: null,
|
||||||
coins_amount: -hand.bet, user_new_amount: coins - hand.bet,
|
action: "BLACKJACK_SPLIT",
|
||||||
});
|
coins_amount: -hand.bet,
|
||||||
p.bank = coins - hand.bet;
|
user_new_amount: coins - hand.bet,
|
||||||
// effective bet size is handled in settlement via hand.doubled flag
|
});
|
||||||
}
|
p.bank = coins - hand.bet;
|
||||||
|
// effective bet size is handled in settlement via hand.doubled flag
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const evt = applyAction(room, userId, action);
|
const evt = applyAction(room, userId, action);
|
||||||
emitToast({ type: `player-${evt}`, userId });
|
emitToast({ type: `player-${evt}`, userId });
|
||||||
emitUpdate("player-action", snapshot(room));
|
emitUpdate("player-action", snapshot(room));
|
||||||
return res.status(200).json({ message: "ok" });
|
return res.status(200).json({ message: "ok" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(400).json({ message: e.message });
|
return res.status(400).json({ message: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Game loop ---
|
// --- Game loop ---
|
||||||
// Simple phase machine that runs regardless of player count.
|
// Simple phase machine that runs regardless of player count.
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (room.status === "betting" && now >= room.phase_ends_at) {
|
if (room.status === "betting" && now >= room.phase_ends_at) {
|
||||||
const hasBets = Object.values(room.players).some(p => p.currentBet >= room.minBet);
|
const hasBets = Object.values(room.players).some((p) => p.currentBet >= room.minBet);
|
||||||
if (!hasBets) {
|
if (!hasBets) {
|
||||||
// Extend betting window if no one bet
|
// Extend betting window if no one bet
|
||||||
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
|
||||||
emitUpdate("betting-extend", snapshot(room));
|
emitUpdate("betting-extend", snapshot(room));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dealInitial(room);
|
dealInitial(room);
|
||||||
autoActions(room);
|
autoActions(room);
|
||||||
emitUpdate("initial-deal", snapshot(room));
|
emitUpdate("initial-deal", snapshot(room));
|
||||||
|
|
||||||
room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer;
|
room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer;
|
||||||
emitUpdate("playing-start", snapshot(room));
|
emitUpdate("playing-start", snapshot(room));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.status === "playing") {
|
if (room.status === "playing") {
|
||||||
// If the per-round playing timer expired, auto-surrender AFKs (you already added this)
|
// If the per-round playing timer expired, auto-surrender AFKs (you already added this)
|
||||||
if (room.phase_ends_at && now >= room.phase_ends_at) {
|
if (room.phase_ends_at && now >= room.phase_ends_at) {
|
||||||
autoTimeoutAFK(now);
|
autoTimeoutAFK(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everyone acted before the timer? Cut short and go straight to dealer.
|
// Everyone acted before the timer? Cut short and go straight to dealer.
|
||||||
if (everyoneDone(room) && !animatingDealer) {
|
if (everyoneDone(room) && !animatingDealer) {
|
||||||
// Set a new server-driven deadline for the reveal pause,
|
// Set a new server-driven deadline for the reveal pause,
|
||||||
// so the client's countdown immediately reflects the phase change.
|
// so the client's countdown immediately reflects the phase change.
|
||||||
room.phase_ends_at = Date.now();
|
room.phase_ends_at = Date.now();
|
||||||
emitUpdate("playing-cut-short", snapshot(room));
|
emitUpdate("playing-cut-short", snapshot(room));
|
||||||
|
|
||||||
// Now run the animated dealer with per-step updates
|
// Now run the animated dealer with per-step updates
|
||||||
runDealerAnimation();
|
runDealerAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.status === "payout" && now >= room.phase_ends_at) {
|
if (room.status === "payout" && now >= room.phase_ends_at) {
|
||||||
// Remove leavers
|
// Remove leavers
|
||||||
for (const userId of Object.keys(room.leavingAfterRound)) {
|
for (const userId of Object.keys(room.leavingAfterRound)) {
|
||||||
delete room.players[userId];
|
delete room.players[userId];
|
||||||
}
|
}
|
||||||
// Prepare next round
|
// Prepare next round
|
||||||
startBetting(room, now);
|
startBetting(room, now);
|
||||||
emitUpdate("new-round", snapshot(room));
|
emitUpdate("new-round", snapshot(room));
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import {erinyesRooms} from "../../game/state.js";
|
import { erinyesRooms } from "../../game/state.js";
|
||||||
import {socketEmit} from "../socket.js";
|
import { socketEmit } from "../socket.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -12,89 +12,91 @@ const router = express.Router();
|
|||||||
* @returns {object} The configured Express router.
|
* @returns {object} The configured Express router.
|
||||||
*/
|
*/
|
||||||
export function erinyesRoutes(client, io) {
|
export function erinyesRoutes(client, io) {
|
||||||
|
// --- Router Management Endpoints
|
||||||
|
|
||||||
// --- Router Management Endpoints
|
router.get("/", (req, res) => {
|
||||||
|
res.status(200).json({ rooms: erinyesRooms });
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get("/:id", (req, res) => {
|
||||||
res.status(200).json({ rooms: erinyesRooms })
|
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) => {
|
router.post("/create", async (req, res) => {
|
||||||
const room = erinyesRooms[req.params.id];
|
const { creatorId } = req.body;
|
||||||
if (room) {
|
if (!creatorId) return res.status(404).json({ message: "Creator ID is required." });
|
||||||
res.status(200).json({ room });
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'Room not found.' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/create', async (req, res) => {
|
if (Object.values(erinyesRooms).some((room) => creatorId === room.host_id || room.players[creatorId])) {
|
||||||
const { creatorId } = req.body;
|
res.status(404).json({ message: "You are already in a room." });
|
||||||
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])) {
|
const creator = await client.users.fetch(creatorId);
|
||||||
res.status(404).json({ message: 'You are already in a room.' });
|
const id = uuidv4();
|
||||||
}
|
|
||||||
|
|
||||||
const creator = await client.users.fetch(creatorId);
|
createRoom({
|
||||||
const id = uuidv4()
|
host_id: creatorId,
|
||||||
|
host_name: creator.globalName,
|
||||||
|
game_rules: {}, // Specific game rules
|
||||||
|
roles: [], // Every role in the game
|
||||||
|
});
|
||||||
|
|
||||||
createRoom({
|
await socketEmit("erinyes-update", {
|
||||||
host_id: creatorId,
|
room: erinyesRooms[id],
|
||||||
host_name: creator.globalName,
|
type: "room-created",
|
||||||
game_rules: {}, // Specific game rules
|
});
|
||||||
roles: [], // Every role in the game
|
res.status(200).json({ room: id });
|
||||||
})
|
});
|
||||||
|
|
||||||
await socketEmit('erinyes-update', { room: erinyesRooms[id], type: 'room-created' });
|
return router;
|
||||||
res.status(200).json({ room: id });
|
|
||||||
})
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRoom(config) {
|
function createRoom(config) {
|
||||||
erinyesRooms[config.id] = {
|
erinyesRooms[config.id] = {
|
||||||
host_id: config.host_id,
|
host_id: config.host_id,
|
||||||
host_name: config.host_name,
|
host_name: config.host_name,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
last_move_at: null,
|
last_move_at: null,
|
||||||
players: {},
|
players: {},
|
||||||
current_player: null,
|
current_player: null,
|
||||||
current_turn: null,
|
current_turn: null,
|
||||||
playing: false,
|
playing: false,
|
||||||
game_rules: createGameRules(config.game_rules),
|
game_rules: createGameRules(config.game_rules),
|
||||||
roles: config.roles,
|
roles: config.roles,
|
||||||
roles_rules: createRolesRules(config.roles),
|
roles_rules: createRolesRules(config.roles),
|
||||||
bonuses: {}
|
bonuses: {},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGameRules(config) {
|
function createGameRules(config) {
|
||||||
return {
|
return {
|
||||||
day_vote_time: config.day_vote_time ?? 60000,
|
day_vote_time: config.day_vote_time ?? 60000,
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRolesRules(roles) {
|
function createRolesRules(roles) {
|
||||||
const roles_rules = {}
|
const roles_rules = {};
|
||||||
|
|
||||||
roles.forEach(role => {
|
roles.forEach((role) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case 'erynie':
|
case "erynie":
|
||||||
roles_rules[role] = {
|
roles_rules[role] = {
|
||||||
//...
|
//...
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
//...
|
//...
|
||||||
default:
|
default:
|
||||||
roles_rules[role] = {
|
roles_rules[role] = {
|
||||||
//...
|
//...
|
||||||
};
|
};
|
||||||
break;
|
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 express from "express";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { uniqueNamesGenerator, adjectives } from 'unique-names-generator';
|
import { uniqueNamesGenerator, adjectives } from "unique-names-generator";
|
||||||
import pkg from 'pokersolver';
|
import pkg from "pokersolver";
|
||||||
const { Hand } = pkg;
|
const { Hand } = pkg;
|
||||||
|
|
||||||
import { pokerRooms } from '../../game/state.js';
|
import { pokerRooms } from "../../game/state.js";
|
||||||
import { initialShuffledCards, getFirstActivePlayerAfterDealer, getNextActivePlayer, checkEndOfBettingRound, checkRoomWinners } from '../../game/poker.js';
|
import {
|
||||||
import { pokerEloHandler } from '../../game/elo.js';
|
initialShuffledCards,
|
||||||
import { getUser, updateUserCoins, insertLog } from '../../database/index.js';
|
getFirstActivePlayerAfterDealer,
|
||||||
|
getNextActivePlayer,
|
||||||
|
checkEndOfBettingRound,
|
||||||
|
checkRoomWinners,
|
||||||
|
} from "../../game/poker.js";
|
||||||
|
import { pokerEloHandler } from "../../game/elo.js";
|
||||||
|
import { getUser, updateUserCoins, insertLog } from "../../database/index.js";
|
||||||
import { sleep } from "openai/core";
|
import { sleep } from "openai/core";
|
||||||
import {client} from "../../bot/client.js";
|
import { client } from "../../bot/client.js";
|
||||||
import {emitPokerToast, emitPokerUpdate} from "../socket.js";
|
import { emitPokerToast, emitPokerUpdate } from "../socket.js";
|
||||||
import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js";
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||||
import {formatAmount} from "../../utils/index.js";
|
import { formatAmount } from "../../utils/index.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -23,474 +29,540 @@ const router = express.Router();
|
|||||||
* @returns {object} The configured Express router.
|
* @returns {object} The configured Express router.
|
||||||
*/
|
*/
|
||||||
export function pokerRoutes(client, io) {
|
export function pokerRoutes(client, io) {
|
||||||
|
// --- Room Management Endpoints ---
|
||||||
|
|
||||||
// --- Room Management Endpoints ---
|
router.get("/", (req, res) => {
|
||||||
|
res.status(200).json({ rooms: pokerRooms });
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get("/:id", (req, res) => {
|
||||||
res.status(200).json({ rooms: pokerRooms });
|
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) => {
|
router.post("/create", async (req, res) => {
|
||||||
const room = pokerRooms[req.params.id];
|
const { creatorId, minBet, fakeMoney } = req.body;
|
||||||
if (room) {
|
if (!creatorId) return res.status(400).json({ message: "Creator ID is required." });
|
||||||
res.status(200).json({ room });
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'Poker room not found.' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/create', async (req, res) => {
|
if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) {
|
||||||
const { creatorId, minBet, fakeMoney } = req.body;
|
return res.status(403).json({ message: "You are already in a poker room." });
|
||||||
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])) {
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||||
return res.status(403).json({ message: 'You are already in a poker room.' });
|
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);
|
pokerRooms[id] = {
|
||||||
const creator = await client.users.fetch(creatorId);
|
id,
|
||||||
const id = uuidv4();
|
host_id: creatorId,
|
||||||
const name = uniqueNamesGenerator({ dictionaries: [adjectives, ['Poker']], separator: ' ', style: 'capital' });
|
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] = {
|
await joinRoom(id, creatorId, io); // Auto-join the creator
|
||||||
id, host_id: creatorId, host_name: creator.globalName || creator.username,
|
await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" });
|
||||||
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
|
try {
|
||||||
await emitPokerUpdate({ room: pokerRooms[id], type: 'room-created' });
|
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 row = new ActionRowBuilder().addComponents(
|
||||||
const generalChannel = guild.channels.cache.find(
|
new ButtonBuilder()
|
||||||
ch => ch.name === 'général' || ch.name === 'general'
|
.setLabel(`Rejoindre la table ${name}`)
|
||||||
);
|
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`)
|
||||||
const embed = new EmbedBuilder()
|
.setStyle(ButtonStyle.Link),
|
||||||
.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(
|
await generalChannel.send({ embeds: [embed], components: [row] });
|
||||||
new ButtonBuilder()
|
} catch (e) {
|
||||||
.setLabel(`Rejoindre la table ${name}`)
|
console.log(e);
|
||||||
.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] });
|
res.status(201).json({ roomId: id });
|
||||||
} catch (e) {
|
});
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
await joinRoom(roomId, userId, io);
|
||||||
const { userId, roomId } = req.body;
|
res.status(200).json({ message: "Successfully joined." });
|
||||||
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);
|
router.post("/accept", async (req, res) => {
|
||||||
res.status(200).json({ message: 'Successfully joined.' });
|
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) => {
|
if (!room.fakeMoney) {
|
||||||
const { hostId, playerId, roomId } = req.body;
|
const userDB = getUser.get(playerId);
|
||||||
const room = pokerRooms[roomId];
|
if (userDB) {
|
||||||
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
|
updateUserCoins.run({
|
||||||
return res.status(403).json({ message: 'Unauthorized or player not in queue.' });
|
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) {
|
room.players[playerId] = room.queue[playerId];
|
||||||
const userDB = getUser.get(playerId);
|
delete room.queue[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];
|
await emitPokerUpdate({ room: room, type: "player-accepted" });
|
||||||
delete room.queue[playerId];
|
res.status(200).json({ message: "Player accepted." });
|
||||||
|
});
|
||||||
|
|
||||||
await emitPokerUpdate({ room: room, type: 'player-accepted' });
|
router.post("/leave", async (req, res) => {
|
||||||
res.status(200).json({ message: 'Player accepted.' });
|
const { userId, roomId } = req.body;
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/leave', async (req, res) => {
|
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||||
const { userId, roomId } = req.body
|
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 (
|
||||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
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)) {
|
try {
|
||||||
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId]
|
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 {
|
await emitPokerUpdate({ type: "player-afk" });
|
||||||
pokerRooms[roomId].players[userId].folded = true
|
return res.status(200);
|
||||||
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' });
|
try {
|
||||||
return res.status(200)
|
updatePlayerCoins(
|
||||||
}
|
pokerRooms[roomId].players[userId],
|
||||||
|
pokerRooms[roomId].players[userId].bank,
|
||||||
|
pokerRooms[roomId].fakeMoney,
|
||||||
|
);
|
||||||
|
delete pokerRooms[roomId].players[userId];
|
||||||
|
|
||||||
try {
|
if (userId === pokerRooms[roomId].host_id) {
|
||||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
|
||||||
delete pokerRooms[roomId].players[userId]
|
if (!newHostId) {
|
||||||
|
delete pokerRooms[roomId];
|
||||||
|
} else {
|
||||||
|
pokerRooms[roomId].host_id = newHostId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (userId === pokerRooms[roomId].host_id) {
|
await emitPokerUpdate({ type: "player-left" });
|
||||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
return res.status(200);
|
||||||
if (!newHostId) {
|
});
|
||||||
delete pokerRooms[roomId]
|
|
||||||
} else {
|
|
||||||
pokerRooms[roomId].host_id = newHostId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
await emitPokerUpdate({ type: 'player-left' });
|
router.post("/kick", async (req, res) => {
|
||||||
return res.status(200)
|
const { commandUserId, userId, roomId } = req.body;
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/kick', async (req, res) => {
|
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||||
const { commandUserId, userId, roomId } = req.body
|
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 (
|
||||||
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
pokerRooms[roomId].playing &&
|
||||||
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: 'Seul l\'host peut kick' })
|
pokerRooms[roomId].current_turn !== null &&
|
||||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
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)) {
|
try {
|
||||||
return res.status(403).send({ message: 'Playing' })
|
updatePlayerCoins(
|
||||||
}
|
pokerRooms[roomId].players[userId],
|
||||||
|
pokerRooms[roomId].players[userId].bank,
|
||||||
|
pokerRooms[roomId].fakeMoney,
|
||||||
|
);
|
||||||
|
delete pokerRooms[roomId].players[userId];
|
||||||
|
|
||||||
try {
|
if (userId === pokerRooms[roomId].host_id) {
|
||||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId);
|
||||||
delete pokerRooms[roomId].players[userId]
|
if (!newHostId) {
|
||||||
|
delete pokerRooms[roomId];
|
||||||
|
} else {
|
||||||
|
pokerRooms[roomId].host_id = newHostId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (userId === pokerRooms[roomId].host_id) {
|
await emitPokerUpdate({ type: "player-kicked" });
|
||||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
return res.status(200);
|
||||||
if (!newHostId) {
|
});
|
||||||
delete pokerRooms[roomId]
|
|
||||||
} else {
|
|
||||||
pokerRooms[roomId].host_id = newHostId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
await emitPokerUpdate({ type: 'player-kicked' });
|
// --- Game Action Endpoints ---
|
||||||
return res.status(200)
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- 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) => {
|
await startNewHand(room, io);
|
||||||
const { roomId } = req.body;
|
res.status(200).json({ message: "Game started." });
|
||||||
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);
|
// NEW: Endpoint to start the next hand
|
||||||
res.status(200).json({ message: 'Game started.' });
|
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("/action/:action", async (req, res) => {
|
||||||
router.post('/next-hand', async (req, res) => {
|
const { playerId, amount, roomId } = req.body;
|
||||||
const { roomId } = req.body;
|
const { action } = req.params;
|
||||||
const room = pokerRooms[roomId];
|
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) => {
|
if (!room || !room.players[playerId] || room.current_player !== playerId) {
|
||||||
const { playerId, amount, roomId } = req.body;
|
return res.status(403).json({ message: "It's not your turn or you are not in this game." });
|
||||||
const { action } = req.params;
|
}
|
||||||
const room = pokerRooms[roomId];
|
|
||||||
|
|
||||||
if (!room || !room.players[playerId] || room.current_player !== playerId) {
|
const player = room.players[playerId];
|
||||||
return res.status(403).json({ message: "It's not your turn or you are not in this game." });
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
player.last_played_turn = room.current_turn;
|
||||||
case 'fold':
|
await checkRoundCompletion(room, io);
|
||||||
player.folded = true;
|
res.status(200).json({ message: `Action '${action}' successful.` });
|
||||||
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;
|
return router;
|
||||||
await checkRoundCompletion(room, io);
|
|
||||||
res.status(200).json({ message: `Action '${action}' successful.` });
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
async function joinRoom(roomId, userId, io) {
|
async function joinRoom(roomId, userId, io) {
|
||||||
const user = await client.users.fetch(userId);
|
const user = await client.users.fetch(userId);
|
||||||
const userDB = getUser.get(userId);
|
const userDB = getUser.get(userId);
|
||||||
const room = pokerRooms[roomId];
|
const room = pokerRooms[roomId];
|
||||||
|
|
||||||
const playerObject = {
|
const playerObject = {
|
||||||
id: userId, globalName: user.globalName || user.username, avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
id: userId,
|
||||||
hand: [], bank: room.minBet, bet: 0, folded: false, allin: false,
|
globalName: user.globalName || user.username,
|
||||||
last_played_turn: null, solve: null
|
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
};
|
hand: [],
|
||||||
|
bank: room.minBet,
|
||||||
|
bet: 0,
|
||||||
|
folded: false,
|
||||||
|
allin: false,
|
||||||
|
last_played_turn: null,
|
||||||
|
solve: null,
|
||||||
|
};
|
||||||
|
|
||||||
if (room.playing) {
|
if (room.playing) {
|
||||||
room.queue[userId] = playerObject;
|
room.queue[userId] = playerObject;
|
||||||
} else {
|
} else {
|
||||||
room.players[userId] = playerObject;
|
room.players[userId] = playerObject;
|
||||||
if (!room.fakeMoney) {
|
if (!room.fakeMoney) {
|
||||||
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
|
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-poker-${Date.now()}`,
|
id: `${userId}-poker-${Date.now()}`,
|
||||||
user_id: userId, target_user_id: null,
|
user_id: userId,
|
||||||
action: 'POKER_JOIN',
|
target_user_id: null,
|
||||||
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
|
action: "POKER_JOIN",
|
||||||
})
|
coins_amount: -room.minBet,
|
||||||
}
|
user_new_amount: userDB.coins - room.minBet,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await emitPokerUpdate({ room: room, type: 'player-joined' });
|
await emitPokerUpdate({ room: room, type: "player-joined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startNewHand(room, io) {
|
async function startNewHand(room, io) {
|
||||||
const playerIds = Object.keys(room.players);
|
const playerIds = Object.keys(room.players);
|
||||||
if (playerIds.length < 2) {
|
if (playerIds.length < 2) {
|
||||||
room.playing = false; // Not enough players to continue
|
room.playing = false; // Not enough players to continue
|
||||||
await emitPokerUpdate({ room: room, type: 'new-hand' });
|
await emitPokerUpdate({ room: room, type: "new-hand" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room.playing = true;
|
room.playing = true;
|
||||||
room.current_turn = 0; // Pre-flop
|
room.current_turn = 0; // Pre-flop
|
||||||
room.pioche = initialShuffledCards();
|
room.pioche = initialShuffledCards();
|
||||||
room.tapis = [];
|
room.tapis = [];
|
||||||
room.winners = [];
|
room.winners = [];
|
||||||
room.waiting_for_restart = false;
|
room.waiting_for_restart = false;
|
||||||
room.highest_bet = 20;
|
room.highest_bet = 20;
|
||||||
room.last_move_at = Date.now();
|
room.last_move_at = Date.now();
|
||||||
|
|
||||||
// Rotate dealer
|
// Rotate dealer
|
||||||
const oldDealerIndex = playerIds.indexOf(room.dealer);
|
const oldDealerIndex = playerIds.indexOf(room.dealer);
|
||||||
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
|
room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length];
|
||||||
|
|
||||||
Object.values(room.players).forEach(p => {
|
Object.values(room.players).forEach((p) => {
|
||||||
p.hand = [room.pioche.pop(), room.pioche.pop()];
|
p.hand = [room.pioche.pop(), room.pioche.pop()];
|
||||||
p.bet = 0; p.folded = false; p.allin = false; p.last_played_turn = null;
|
p.bet = 0;
|
||||||
});
|
p.folded = false;
|
||||||
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
|
p.allin = false;
|
||||||
|
p.last_played_turn = null;
|
||||||
|
});
|
||||||
|
updatePlayerHandSolves(room); // NEW: Calculate initial hand strength
|
||||||
|
|
||||||
// Handle blinds based on new dealer
|
// Handle blinds based on new dealer
|
||||||
const dealerIndex = playerIds.indexOf(room.dealer);
|
const dealerIndex = playerIds.indexOf(room.dealer);
|
||||||
const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]];
|
const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]];
|
||||||
const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]];
|
const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]];
|
||||||
room.sb = sbPlayer.id;
|
room.sb = sbPlayer.id;
|
||||||
room.bb = bbPlayer.id;
|
room.bb = bbPlayer.id;
|
||||||
|
|
||||||
sbPlayer.bank -= 10; sbPlayer.bet = 10;
|
sbPlayer.bank -= 10;
|
||||||
bbPlayer.bank -= 20; bbPlayer.bet = 20;
|
sbPlayer.bet = 10;
|
||||||
|
bbPlayer.bank -= 20;
|
||||||
|
bbPlayer.bet = 20;
|
||||||
|
|
||||||
bbPlayer.last_played_turn = 0;
|
bbPlayer.last_played_turn = 0;
|
||||||
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
|
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
|
||||||
await emitPokerUpdate({ room: room, type: 'room-started' });
|
await emitPokerUpdate({ room: room, type: "room-started" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkRoundCompletion(room, io) {
|
async function checkRoundCompletion(room, io) {
|
||||||
room.last_move_at = Date.now();
|
room.last_move_at = Date.now();
|
||||||
const roundResult = checkEndOfBettingRound(room);
|
const roundResult = checkEndOfBettingRound(room);
|
||||||
|
|
||||||
if (roundResult.endRound) {
|
if (roundResult.endRound) {
|
||||||
if (roundResult.winner) {
|
if (roundResult.winner) {
|
||||||
await handleShowdown(room, io, [roundResult.winner]);
|
await handleShowdown(room, io, [roundResult.winner]);
|
||||||
} else {
|
} else {
|
||||||
await advanceToNextPhase(room, io, roundResult.nextPhase);
|
await advanceToNextPhase(room, io, roundResult.nextPhase);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
room.current_player = getNextActivePlayer(room);
|
room.current_player = getNextActivePlayer(room);
|
||||||
await emitPokerUpdate({ room: room, type: 'round-continue' });
|
await emitPokerUpdate({ room: room, type: "round-continue" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function advanceToNextPhase(room, io, phase) {
|
async function advanceToNextPhase(room, io, phase) {
|
||||||
Object.values(room.players).forEach(p => { if (!p.folded) p.last_played_turn = null; });
|
Object.values(room.players).forEach((p) => {
|
||||||
|
if (!p.folded) p.last_played_turn = null;
|
||||||
|
});
|
||||||
|
|
||||||
switch(phase) {
|
switch (phase) {
|
||||||
case 'flop':
|
case "flop":
|
||||||
room.current_turn = 1;
|
room.current_turn = 1;
|
||||||
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
|
room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop());
|
||||||
break;
|
break;
|
||||||
case 'turn':
|
case "turn":
|
||||||
room.current_turn = 2;
|
room.current_turn = 2;
|
||||||
room.tapis.push(room.pioche.pop());
|
room.tapis.push(room.pioche.pop());
|
||||||
break;
|
break;
|
||||||
case 'river':
|
case "river":
|
||||||
room.current_turn = 3;
|
room.current_turn = 3;
|
||||||
room.tapis.push(room.pioche.pop());
|
room.tapis.push(room.pioche.pop());
|
||||||
break;
|
break;
|
||||||
case 'showdown':
|
case "showdown":
|
||||||
await handleShowdown(room, io, checkRoomWinners(room));
|
await handleShowdown(room, io, checkRoomWinners(room));
|
||||||
return;
|
return;
|
||||||
case 'progressive-showdown':
|
case "progressive-showdown":
|
||||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
|
||||||
while(room.tapis.length < 5) {
|
while (room.tapis.length < 5) {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
room.tapis.push(room.pioche.pop());
|
room.tapis.push(room.pioche.pop());
|
||||||
updatePlayerHandSolves(room);
|
updatePlayerHandSolves(room);
|
||||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
await emitPokerUpdate({ room: room, type: "progressive-showdown" });
|
||||||
}
|
}
|
||||||
await handleShowdown(room, io, checkRoomWinners(room));
|
await handleShowdown(room, io, checkRoomWinners(room));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
|
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
|
||||||
room.current_player = getFirstActivePlayerAfterDealer(room);
|
room.current_player = getFirstActivePlayerAfterDealer(room);
|
||||||
await emitPokerUpdate({ room: room, type: 'phase-advanced' });
|
await emitPokerUpdate({ room: room, type: "phase-advanced" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleShowdown(room, io, winners) {
|
async function handleShowdown(room, io, winners) {
|
||||||
room.current_turn = 4;
|
room.current_turn = 4;
|
||||||
room.playing = false;
|
room.playing = false;
|
||||||
room.waiting_for_restart = true;
|
room.waiting_for_restart = true;
|
||||||
room.winners = winners;
|
room.winners = winners;
|
||||||
room.current_player = null;
|
room.current_player = null;
|
||||||
|
|
||||||
let totalPot = 0;
|
let totalPot = 0;
|
||||||
Object.values(room.players).forEach(p => { totalPot += p.bet; });
|
Object.values(room.players).forEach((p) => {
|
||||||
|
totalPot += p.bet;
|
||||||
|
});
|
||||||
|
|
||||||
const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0;
|
const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0;
|
||||||
|
|
||||||
winners.forEach(winnerId => {
|
winners.forEach((winnerId) => {
|
||||||
const winnerPlayer = room.players[winnerId];
|
const winnerPlayer = room.players[winnerId];
|
||||||
if(winnerPlayer) {
|
if (winnerPlayer) {
|
||||||
winnerPlayer.bank += winAmount;
|
winnerPlayer.bank += winAmount;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await clearAfkPlayers(room);
|
await clearAfkPlayers(room);
|
||||||
|
|
||||||
//await pokerEloHandler(room);
|
//await pokerEloHandler(room);
|
||||||
await emitPokerUpdate({ room: room, type: 'showdown' });
|
await emitPokerUpdate({ room: room, type: "showdown" });
|
||||||
await emitPokerToast({
|
await emitPokerToast({
|
||||||
type: 'player-winner',
|
type: "player-winner",
|
||||||
playerIds: winners,
|
playerIds: winners,
|
||||||
roomId: room.id,
|
roomId: room.id,
|
||||||
amount: winAmount,
|
amount: winAmount,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Function to calculate and update hand strength for all players
|
// NEW: Function to calculate and update hand strength for all players
|
||||||
function updatePlayerHandSolves(room) {
|
function updatePlayerHandSolves(room) {
|
||||||
const communityCards = room.tapis;
|
const communityCards = room.tapis;
|
||||||
for (const player of Object.values(room.players)) {
|
for (const player of Object.values(room.players)) {
|
||||||
if (!player.folded) {
|
if (!player.folded) {
|
||||||
const allCards = [...communityCards, ...player.hand];
|
const allCards = [...communityCards, ...player.hand];
|
||||||
player.solve = Hand.solve(allCards).descr;
|
player.solve = Hand.solve(allCards).descr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlayerCoins(player, amount, isFake) {
|
function updatePlayerCoins(player, amount, isFake) {
|
||||||
if (isFake) return;
|
if (isFake) return;
|
||||||
const user = getUser.get(player.id);
|
const user = getUser.get(player.id);
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
const userDB = getUser.get(player.id);
|
const userDB = getUser.get(player.id);
|
||||||
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
|
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${player.id}-poker-${Date.now()}`,
|
id: `${player.id}-poker-${Date.now()}`,
|
||||||
user_id: player.id, target_user_id: null,
|
user_id: player.id,
|
||||||
action: `POKER_${amount > 0 ? 'WIN' : 'LOSE'}`,
|
target_user_id: null,
|
||||||
coins_amount: amount, user_new_amount: userDB.coins + amount,
|
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
|
||||||
});
|
coins_amount: amount,
|
||||||
|
user_new_amount: userDB.coins + amount,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearAfkPlayers(room) {
|
async function clearAfkPlayers(room) {
|
||||||
Object.keys(room.afk).forEach(playerId => {
|
Object.keys(room.afk).forEach((playerId) => {
|
||||||
if (room.players[playerId]) {
|
if (room.players[playerId]) {
|
||||||
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
|
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
|
||||||
delete room.players[playerId];
|
delete room.players[playerId];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
room.afk = {};
|
room.afk = {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
import express from 'express';
|
import express from "express";
|
||||||
|
|
||||||
// --- Game Logic Imports ---
|
// --- Game Logic Imports ---
|
||||||
import {
|
import {
|
||||||
createDeck, shuffle, deal, isValidMove, moveCard, drawCard,
|
createDeck,
|
||||||
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves
|
shuffle,
|
||||||
} from '../../game/solitaire.js';
|
deal,
|
||||||
|
isValidMove,
|
||||||
|
moveCard,
|
||||||
|
drawCard,
|
||||||
|
checkWinCondition,
|
||||||
|
createSeededRNG,
|
||||||
|
seededShuffle,
|
||||||
|
undoMove,
|
||||||
|
draw3Cards,
|
||||||
|
checkAutoSolve,
|
||||||
|
autoSolveMoves,
|
||||||
|
} from "../../game/solitaire.js";
|
||||||
|
|
||||||
// --- Game State & Database Imports ---
|
// --- Game State & Database Imports ---
|
||||||
import { activeSolitaireGames } from '../../game/state.js';
|
import { activeSolitaireGames } from "../../game/state.js";
|
||||||
import {
|
import {
|
||||||
getSOTD, getUser, insertSOTDStats, deleteUserSOTDStats,
|
getSOTD,
|
||||||
getUserSOTDStats, updateUserCoins, insertLog, getAllSOTDStats
|
getUser,
|
||||||
} from '../../database/index.js';
|
insertSOTDStats,
|
||||||
import {socketEmit} from "../socket.js";
|
deleteUserSOTDStats,
|
||||||
|
getUserSOTDStats,
|
||||||
|
updateUserCoins,
|
||||||
|
insertLog,
|
||||||
|
getAllSOTDStats,
|
||||||
|
} from "../../database/index.js";
|
||||||
|
import { socketEmit } from "../socket.js";
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -24,248 +41,262 @@ const router = express.Router();
|
|||||||
* @returns {object} The configured Express router.
|
* @returns {object} The configured Express router.
|
||||||
*/
|
*/
|
||||||
export function solitaireRoutes(client, io) {
|
export function solitaireRoutes(client, io) {
|
||||||
|
// --- Game Initialization Endpoints ---
|
||||||
|
|
||||||
// --- Game Initialization Endpoints ---
|
router.post("/start", (req, res) => {
|
||||||
|
const { userId, userSeed, hardMode } = req.body;
|
||||||
|
if (!userId) return res.status(400).json({ error: "User ID is required." });
|
||||||
|
|
||||||
router.post('/start', (req, res) => {
|
// If a game already exists for the user, return it instead of creating a new one.
|
||||||
const { userId, userSeed, hardMode } = req.body;
|
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
|
||||||
if (!userId) return res.status(400).json({ error: 'User ID is required.' });
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
gameState: activeSolitaireGames[userId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If a game already exists for the user, return it instead of creating a new one.
|
let deck, seed;
|
||||||
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
|
if (userSeed) {
|
||||||
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
|
// 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;
|
let numericSeed = 0;
|
||||||
if (userSeed) {
|
for (let i = 0; i < seed.length; i++) {
|
||||||
// Use the provided seed to create a deterministic game
|
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xffffffff;
|
||||||
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;
|
const rng = createSeededRNG(numericSeed);
|
||||||
for (let i = 0; i < seed.length; i++) {
|
deck = seededShuffle(createDeck(), rng);
|
||||||
numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xFFFFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rng = createSeededRNG(numericSeed);
|
const gameState = deal(deck);
|
||||||
deck = seededShuffle(createDeck(), rng);
|
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);
|
res.json({ success: true, gameState });
|
||||||
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 });
|
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.' });
|
return res.status(404).json({ error: 'User not found.' });
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
if (activeSolitaireGames[userId]?.isSOTD) {
|
if (activeSolitaireGames[userId]?.isSOTD) {
|
||||||
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
|
return res.json({
|
||||||
}
|
success: true,
|
||||||
|
gameState: activeSolitaireGames[userId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const sotd = getSOTD.get();
|
const sotd = getSOTD.get();
|
||||||
if (!sotd) {
|
if (!sotd) {
|
||||||
return res.status(500).json({ error: 'Solitaire of the Day is not configured.'});
|
return res.status(500).json({ error: "Solitaire of the Day is not configured." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameState = {
|
const gameState = {
|
||||||
tableauPiles: JSON.parse(sotd.tableauPiles),
|
tableauPiles: JSON.parse(sotd.tableauPiles),
|
||||||
foundationPiles: JSON.parse(sotd.foundationPiles),
|
foundationPiles: JSON.parse(sotd.foundationPiles),
|
||||||
stockPile: JSON.parse(sotd.stockPile),
|
stockPile: JSON.parse(sotd.stockPile),
|
||||||
wastePile: JSON.parse(sotd.wastePile),
|
wastePile: JSON.parse(sotd.wastePile),
|
||||||
isDone: false,
|
isDone: false,
|
||||||
isSOTD: true,
|
isSOTD: true,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
endTime: null,
|
endTime: null,
|
||||||
moves: 0,
|
moves: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
seed: sotd.seed,
|
seed: sotd.seed,
|
||||||
hist: [],
|
hist: [],
|
||||||
hardMode: false,
|
hardMode: false,
|
||||||
autocompleting: false,
|
autocompleting: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
activeSolitaireGames[userId] = gameState;
|
activeSolitaireGames[userId] = gameState;
|
||||||
res.json({ success: true, gameState });
|
res.json({ success: true, gameState });
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Game State & Action Endpoints ---
|
// --- Game State & Action Endpoints ---
|
||||||
|
|
||||||
router.get('/sotd/rankings', (req, res) => {
|
router.get("/sotd/rankings", (req, res) => {
|
||||||
try {
|
try {
|
||||||
const rankings = getAllSOTDStats.all();
|
const rankings = getAllSOTDStats.all();
|
||||||
res.json({ rankings });
|
res.json({ rankings });
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: "Failed to fetch SOTD rankings."});
|
res.status(500).json({ error: "Failed to fetch SOTD rankings." });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/state/:userId', (req, res) => {
|
router.get("/state/:userId", (req, res) => {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const gameState = activeSolitaireGames[userId];
|
const gameState = activeSolitaireGames[userId];
|
||||||
if (gameState) {
|
if (gameState) {
|
||||||
res.json({ success: true, gameState });
|
res.json({ success: true, gameState });
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: 'No active game found for this user.' });
|
res.status(404).json({ error: "No active game found for this user." });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/reset', (req, res) => {
|
router.post("/reset", (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
if (activeSolitaireGames[userId]) {
|
if (activeSolitaireGames[userId]) {
|
||||||
delete activeSolitaireGames[userId];
|
delete activeSolitaireGames[userId];
|
||||||
}
|
}
|
||||||
res.json({ success: true, message: "Game reset."});
|
res.json({ success: true, message: "Game reset." });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/move', async (req, res) => {
|
router.post("/move", async (req, res) => {
|
||||||
const { userId, ...moveData } = req.body;
|
const { userId, ...moveData } = req.body;
|
||||||
const gameState = activeSolitaireGames[userId];
|
const gameState = activeSolitaireGames[userId];
|
||||||
|
|
||||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||||
|
|
||||||
if (isValidMove(gameState, moveData)) {
|
if (isValidMove(gameState, moveData)) {
|
||||||
moveCard(gameState, moveData);
|
moveCard(gameState, moveData);
|
||||||
updateGameStats(gameState, 'move', moveData);
|
updateGameStats(gameState, "move", moveData);
|
||||||
|
|
||||||
if (!gameState.autocompleting) {
|
if (!gameState.autocompleting) {
|
||||||
const canAutoSolve = checkAutoSolve(gameState);
|
const canAutoSolve = checkAutoSolve(gameState);
|
||||||
if (canAutoSolve) {
|
if (canAutoSolve) {
|
||||||
gameState.autocompleting = true;
|
gameState.autocompleting = true;
|
||||||
autoSolveMoves(userId, gameState)
|
autoSolveMoves(userId, gameState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const win = checkWinCondition(gameState);
|
const win = checkWinCondition(gameState);
|
||||||
if (win) {
|
if (win) {
|
||||||
gameState.isDone = true;
|
gameState.isDone = true;
|
||||||
await handleWin(userId, gameState, io);
|
await handleWin(userId, gameState, io);
|
||||||
}
|
}
|
||||||
res.json({ success: true, gameState, win });
|
res.json({ success: true, gameState, win });
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({ error: 'Invalid move' });
|
res.status(400).json({ error: "Invalid move" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/draw', (req, res) => {
|
router.post("/draw", (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
const gameState = activeSolitaireGames[userId];
|
const gameState = activeSolitaireGames[userId];
|
||||||
|
|
||||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||||
|
|
||||||
if (gameState.hardMode) {
|
if (gameState.hardMode) {
|
||||||
draw3Cards(gameState);
|
draw3Cards(gameState);
|
||||||
} else {
|
} else {
|
||||||
drawCard(gameState);
|
drawCard(gameState);
|
||||||
}
|
}
|
||||||
updateGameStats(gameState, 'draw');
|
updateGameStats(gameState, "draw");
|
||||||
res.json({ success: true, gameState });
|
res.json({ success: true, gameState });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/undo', (req, res) => {
|
router.post("/undo", (req, res) => {
|
||||||
const { userId } = req.body;
|
const { userId } = req.body;
|
||||||
const gameState = activeSolitaireGames[userId];
|
const gameState = activeSolitaireGames[userId];
|
||||||
|
|
||||||
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
|
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||||
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
|
if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." });
|
||||||
if (gameState.hist.length === 0) return res.status(400).json({ error: 'No moves to undo.'});
|
if (gameState.hist.length === 0) return res.status(400).json({ error: "No moves to undo." });
|
||||||
|
|
||||||
undoMove(gameState);
|
undoMove(gameState);
|
||||||
res.json({ success: true, gameState });
|
res.json({ success: true, gameState });
|
||||||
})
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
|
|
||||||
/** Updates game stats like moves and score after an action. */
|
/** Updates game stats like moves and score after an action. */
|
||||||
function updateGameStats(gameState, actionType, moveData = {}) {
|
function updateGameStats(gameState, actionType, moveData = {}) {
|
||||||
// if (!gameState.isSOTD) return; // Only track stats for SOTD
|
// if (!gameState.isSOTD) return; // Only track stats for SOTD
|
||||||
|
|
||||||
gameState.moves++;
|
gameState.moves++;
|
||||||
if (actionType === 'move') {
|
if (actionType === "move") {
|
||||||
if (moveData.destPileType === 'foundationPiles') {
|
if (moveData.destPileType === "foundationPiles") {
|
||||||
gameState.score += 10; // Move card to foundation
|
gameState.score += 10; // Move card to foundation
|
||||||
}
|
}
|
||||||
if (moveData.sourcePileType === 'foundationPiles') {
|
if (moveData.sourcePileType === "foundationPiles") {
|
||||||
gameState.score -= 15; // Move card from foundation (penalty)
|
gameState.score -= 15; // Move card from foundation (penalty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(actionType === 'draw' && gameState.wastePile.length === 0) {
|
if (actionType === "draw" && gameState.wastePile.length === 0) {
|
||||||
// Penalty for cycling through an empty stock pile
|
// Penalty for cycling through an empty stock pile
|
||||||
gameState.score -= 5;
|
gameState.score -= 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handles the logic when a game is won. */
|
/** Handles the logic when a game is won. */
|
||||||
async function handleWin(userId, gameState, io) {
|
async function handleWin(userId, gameState, io) {
|
||||||
const currentUser = getUser.get(userId);
|
const currentUser = getUser.get(userId);
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
|
||||||
if (gameState.hardMode) {
|
if (gameState.hardMode) {
|
||||||
const bonus = 100;
|
const bonus = 100;
|
||||||
const newCoins = currentUser.coins + bonus;
|
const newCoins = currentUser.coins + bonus;
|
||||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-hardmode-solitaire-${Date.now()}`, user_id: userId,
|
id: `${userId}-hardmode-solitaire-${Date.now()}`,
|
||||||
action: 'HARDMODE_SOLITAIRE_WIN', target_user_id: null,
|
user_id: userId,
|
||||||
coins_amount: bonus, user_new_amount: newCoins,
|
action: "HARDMODE_SOLITAIRE_WIN",
|
||||||
});
|
target_user_id: null,
|
||||||
await socketEmit('data-updated', { table: 'users' });
|
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();
|
gameState.endTime = Date.now();
|
||||||
const timeTaken = gameState.endTime - gameState.startTime;
|
const timeTaken = gameState.endTime - gameState.startTime;
|
||||||
|
|
||||||
const existingStats = getUserSOTDStats.get(userId);
|
const existingStats = getUserSOTDStats.get(userId);
|
||||||
|
|
||||||
if (!existingStats) {
|
if (!existingStats) {
|
||||||
// First time completing the SOTD, grant bonus coins
|
// First time completing the SOTD, grant bonus coins
|
||||||
const bonus = 1000;
|
const bonus = 1000;
|
||||||
const newCoins = currentUser.coins + bonus;
|
const newCoins = currentUser.coins + bonus;
|
||||||
updateUserCoins.run({ id: userId, coins: newCoins });
|
updateUserCoins.run({ id: userId, coins: newCoins });
|
||||||
insertLog.run({
|
insertLog.run({
|
||||||
id: `${userId}-sotd-complete-${Date.now()}`, user_id: userId,
|
id: `${userId}-sotd-complete-${Date.now()}`,
|
||||||
action: 'SOTD_WIN', target_user_id: null,
|
user_id: userId,
|
||||||
coins_amount: bonus, user_new_amount: newCoins,
|
action: "SOTD_WIN",
|
||||||
});
|
target_user_id: null,
|
||||||
await socketEmit('data-updated', { table: 'users' });
|
coins_amount: bonus,
|
||||||
}
|
user_new_amount: newCoins,
|
||||||
|
});
|
||||||
|
await socketEmit("data-updated", { table: "users" });
|
||||||
|
}
|
||||||
|
|
||||||
// Save the score if it's better than the previous one
|
// Save the score if it's better than the previous one
|
||||||
const isNewBest = !existingStats ||
|
const isNewBest =
|
||||||
gameState.score > existingStats.score ||
|
!existingStats ||
|
||||||
(gameState.score === existingStats.score && gameState.moves < existingStats.moves) ||
|
gameState.score > existingStats.score ||
|
||||||
(gameState.score === existingStats.score && gameState.moves === existingStats.moves && timeTaken < existingStats.time);
|
(gameState.score === existingStats.score && gameState.moves < existingStats.moves) ||
|
||||||
|
(gameState.score === existingStats.score &&
|
||||||
|
gameState.moves === existingStats.moves &&
|
||||||
|
timeTaken < existingStats.time);
|
||||||
|
|
||||||
if (isNewBest) {
|
if (isNewBest) {
|
||||||
deleteUserSOTDStats.run(userId)
|
deleteUserSOTDStats.run(userId);
|
||||||
insertSOTDStats.run({
|
insertSOTDStats.run({
|
||||||
id: userId, user_id: userId,
|
id: userId,
|
||||||
time: timeTaken,
|
user_id: userId,
|
||||||
moves: gameState.moves,
|
time: timeTaken,
|
||||||
score: gameState.score,
|
moves: gameState.moves,
|
||||||
});
|
score: gameState.score,
|
||||||
await socketEmit('sotd-update')
|
});
|
||||||
console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`);
|
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 {
|
import {
|
||||||
activeTicTacToeGames,
|
activeTicTacToeGames,
|
||||||
tictactoeQueue,
|
tictactoeQueue,
|
||||||
activeConnect4Games,
|
activeConnect4Games,
|
||||||
connect4Queue,
|
connect4Queue,
|
||||||
queueMessagesEndpoints, activePredis
|
queueMessagesEndpoints,
|
||||||
} from '../game/state.js';
|
activePredis,
|
||||||
import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js';
|
} from "../game/state.js";
|
||||||
import { eloHandler } from '../game/elo.js';
|
import {
|
||||||
|
createConnect4Board,
|
||||||
|
formatConnect4BoardForDiscord,
|
||||||
|
checkConnect4Win,
|
||||||
|
checkConnect4Draw,
|
||||||
|
C4_ROWS,
|
||||||
|
} from "../game/various.js";
|
||||||
|
import { eloHandler } from "../game/elo.js";
|
||||||
import { getUser } from "../database/index.js";
|
import { getUser } from "../database/index.js";
|
||||||
|
|
||||||
// --- Module-level State ---
|
// --- Module-level State ---
|
||||||
@@ -16,70 +23,73 @@ let io;
|
|||||||
// --- Main Initialization Function ---
|
// --- Main Initialization Function ---
|
||||||
|
|
||||||
export function initializeSocket(server, client) {
|
export function initializeSocket(server, client) {
|
||||||
io = server;
|
io = server;
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on("connection", (socket) => {
|
||||||
socket.on('user-connected', async (userId) => {
|
socket.on("user-connected", async (userId) => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
await refreshQueuesForUser(userId, client);
|
await refreshQueuesForUser(userId, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
registerTicTacToeEvents(socket, client);
|
registerTicTacToeEvents(socket, client);
|
||||||
registerConnect4Events(socket, client);
|
registerConnect4Events(socket, client);
|
||||||
|
|
||||||
socket.on('tictactoe:queue:leave', async ({ discordId }) => await refreshQueuesForUser(discordId, client));
|
socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
|
||||||
|
|
||||||
// catch tab kills / network drops
|
// catch tab kills / network drops
|
||||||
socket.on('disconnecting', async () => {
|
socket.on("disconnecting", async () => {
|
||||||
const discordId = socket.handshake.auth?.discordId; // or your mapping
|
const discordId = socket.handshake.auth?.discordId; // or your mapping
|
||||||
await refreshQueuesForUser(discordId, client);
|
await refreshQueuesForUser(discordId, client);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on("disconnect", () => {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(cleanupStaleGames, 5 * 60 * 1000);
|
setInterval(cleanupStaleGames, 5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSocketIo() {
|
export function getSocketIo() {
|
||||||
return io;
|
return io;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Event Registration ---
|
// --- Event Registration ---
|
||||||
|
|
||||||
function registerTicTacToeEvents(socket, client) {
|
function registerTicTacToeEvents(socket, client) {
|
||||||
socket.on('tictactoeconnection', (e) => refreshQueuesForUser(e.id, client));
|
socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client));
|
||||||
socket.on('tictactoequeue', (e) => onQueueJoin(client, 'tictactoe', e.playerId));
|
socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId));
|
||||||
socket.on('tictactoeplaying', (e) => onTicTacToeMove(client, e));
|
socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e));
|
||||||
socket.on('tictactoegameOver', (e) => onGameOver(client, 'tictactoe', e.playerId, e.winner));
|
socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner));
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerConnect4Events(socket, client) {
|
function registerConnect4Events(socket, client) {
|
||||||
socket.on('connect4connection', (e) => refreshQueuesForUser(e.id, client));
|
socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client));
|
||||||
socket.on('connect4queue', (e) => onQueueJoin(client, 'connect4', e.playerId));
|
socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId));
|
||||||
socket.on('connect4playing', (e) => onConnect4Move(client, e));
|
socket.on("connect4playing", (e) => onConnect4Move(client, e));
|
||||||
socket.on('connect4NoTime', (e) => onGameOver(client, 'connect4', e.playerId, e.winner, '(temps écoulé)'));
|
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Core Handlers (Preserving Original Logic) ---
|
// --- Core Handlers (Preserving Original Logic) ---
|
||||||
|
|
||||||
async function onQueueJoin(client, gameType, playerId) {
|
async function onQueueJoin(client, gameType, playerId) {
|
||||||
if (!playerId) return;
|
if (!playerId) return;
|
||||||
const { queue, activeGames, title, url } = getGameAssets(gameType);
|
const { queue, activeGames, title, url } = getGameAssets(gameType);
|
||||||
|
|
||||||
if (queue.includes(playerId) || Object.values(activeGames).some(g => g.p1.id === playerId || g.p2.id === playerId)) {
|
if (
|
||||||
return;
|
queue.includes(playerId) ||
|
||||||
}
|
Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
queue.push(playerId);
|
queue.push(playerId);
|
||||||
console.log(`[${title}] Player ${playerId} joined the queue.`);
|
console.log(`[${title}] Player ${playerId} joined the queue.`);
|
||||||
|
|
||||||
if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url);
|
if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url);
|
||||||
if (queue.length >= 2) await createGame(client, gameType);
|
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.
|
* @returns {boolean} - True if the player has won, false otherwise.
|
||||||
*/
|
*/
|
||||||
function checkTicTacToeWin(moves) {
|
function checkTicTacToeWin(moves) {
|
||||||
const winningCombinations = [
|
const winningCombinations = [
|
||||||
[1, 2, 3], [4, 5, 6], [7, 8, 9], // Rows
|
[1, 2, 3],
|
||||||
[1, 4, 7], [2, 5, 8], [3, 6, 9], // Columns
|
[4, 5, 6],
|
||||||
[1, 5, 9], [3, 5, 7] // Diagonals
|
[7, 8, 9], // Rows
|
||||||
];
|
[1, 4, 7],
|
||||||
for (const combination of winningCombinations) {
|
[2, 5, 8],
|
||||||
if (combination.every(num => moves.includes(num))) {
|
[3, 6, 9], // Columns
|
||||||
return true;
|
[1, 5, 9],
|
||||||
}
|
[3, 5, 7], // Diagonals
|
||||||
}
|
];
|
||||||
return false;
|
for (const combination of winningCombinations) {
|
||||||
|
if (combination.every((num) => moves.includes(num))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onTicTacToeMove(client, eventData) {
|
async function onTicTacToeMove(client, eventData) {
|
||||||
const { playerId, value, boxId } = eventData;
|
const { playerId, value, boxId } = eventData;
|
||||||
const lobby = Object.values(activeTicTacToeGames).find(g => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver);
|
const lobby = Object.values(activeTicTacToeGames).find(
|
||||||
if (!lobby) return;
|
(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 isP1Turn = lobby.sum % 2 === 1 && value === "X" && lobby.p1.id === playerId;
|
||||||
const isP2Turn = lobby.sum % 2 === 0 && value === 'O' && lobby.p2.id === playerId;
|
const isP2Turn = lobby.sum % 2 === 0 && value === "O" && lobby.p2.id === playerId;
|
||||||
|
|
||||||
if (isP1Turn || isP2Turn) {
|
if (isP1Turn || isP2Turn) {
|
||||||
const playerMoves = isP1Turn ? lobby.xs : lobby.os;
|
const playerMoves = isP1Turn ? lobby.xs : lobby.os;
|
||||||
playerMoves.push(boxId);
|
playerMoves.push(boxId);
|
||||||
lobby.sum++;
|
lobby.sum++;
|
||||||
lobby.lastmove = Date.now();
|
lobby.lastmove = Date.now();
|
||||||
|
|
||||||
if (isP1Turn) lobby.p1.move = boxId
|
if (isP1Turn) lobby.p1.move = boxId;
|
||||||
if (isP2Turn) lobby.p2.move = boxId
|
if (isP2Turn) lobby.p2.move = boxId;
|
||||||
|
|
||||||
io.emit('tictactoeplaying', { allPlayers: Object.values(activeTicTacToeGames) });
|
io.emit("tictactoeplaying", {
|
||||||
const hasWon = checkTicTacToeWin(playerMoves);
|
allPlayers: Object.values(activeTicTacToeGames),
|
||||||
if (hasWon) {
|
});
|
||||||
// The current player has won. End the game.
|
const hasWon = checkTicTacToeWin(playerMoves);
|
||||||
await onGameOver(client, 'tictactoe', playerId, playerId);
|
if (hasWon) {
|
||||||
} else if (lobby.sum > 9) {
|
// The current player has won. End the game.
|
||||||
// It's a draw (9 moves made, sum is now 10). End the game.
|
await onGameOver(client, "tictactoe", playerId, playerId);
|
||||||
await onGameOver(client, 'tictactoe', playerId, null); // null winner for a draw
|
} else if (lobby.sum > 9) {
|
||||||
} else {
|
// It's a draw (9 moves made, sum is now 10). End the game.
|
||||||
// The game continues. Update the state and notify clients.
|
await onGameOver(client, "tictactoe", playerId, null); // null winner for a draw
|
||||||
await updateDiscordMessage(client, lobby, 'Tic Tac Toe');
|
} else {
|
||||||
}
|
// The game continues. Update the state and notify clients.
|
||||||
}
|
await updateDiscordMessage(client, lobby, "Tic Tac Toe");
|
||||||
await emitQueueUpdate(client, 'tictactoe');
|
}
|
||||||
|
}
|
||||||
|
await emitQueueUpdate(client, "tictactoe");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onConnect4Move(client, eventData) {
|
async function onConnect4Move(client, eventData) {
|
||||||
const { playerId, col } = eventData;
|
const { playerId, col } = eventData;
|
||||||
const lobby = Object.values(activeConnect4Games).find(l => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver);
|
const lobby = Object.values(activeConnect4Games).find(
|
||||||
if (!lobby || lobby.turn !== playerId) return;
|
(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;
|
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
|
||||||
let row;
|
let row;
|
||||||
for (row = C4_ROWS - 1; row >= 0; row--) {
|
for (row = C4_ROWS - 1; row >= 0; row--) {
|
||||||
if (lobby.board[row][col] === null) {
|
if (lobby.board[row][col] === null) {
|
||||||
lobby.board[row][col] = player.val;
|
lobby.board[row][col] = player.val;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (row < 0) return;
|
if (row < 0) return;
|
||||||
|
|
||||||
lobby.lastmove = Date.now();
|
lobby.lastmove = Date.now();
|
||||||
const winCheck = checkConnect4Win(lobby.board, player.val);
|
const winCheck = checkConnect4Win(lobby.board, player.val);
|
||||||
|
|
||||||
let winnerId = null;
|
let winnerId = null;
|
||||||
if (winCheck.win) {
|
if (winCheck.win) {
|
||||||
lobby.winningPieces = winCheck.pieces;
|
lobby.winningPieces = winCheck.pieces;
|
||||||
winnerId = player.id;
|
winnerId = player.id;
|
||||||
} else if (checkConnect4Draw(lobby.board)) {
|
} else if (checkConnect4Draw(lobby.board)) {
|
||||||
winnerId = null; // Represents a draw
|
winnerId = null; // Represents a draw
|
||||||
} else {
|
} else {
|
||||||
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
|
lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id;
|
||||||
io.emit('connect4playing', { allPlayers: Object.values(activeConnect4Games) });
|
io.emit("connect4playing", {
|
||||||
await emitQueueUpdate(client, 'connact4');
|
allPlayers: Object.values(activeConnect4Games),
|
||||||
await updateDiscordMessage(client, lobby, 'Puissance 4');
|
});
|
||||||
return;
|
await emitQueueUpdate(client, "connact4");
|
||||||
}
|
await updateDiscordMessage(client, lobby, "Puissance 4");
|
||||||
await onGameOver(client, 'connect4', playerId, winnerId);
|
return;
|
||||||
|
}
|
||||||
|
await onGameOver(client, "connect4", playerId, winnerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onGameOver(client, gameType, playerId, winnerId, reason = '') {
|
async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
|
||||||
const { activeGames, title } = getGameAssets(gameType);
|
const { activeGames, title } = getGameAssets(gameType);
|
||||||
const gameKey = Object.keys(activeGames).find(key => key.includes(playerId));
|
const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId));
|
||||||
const game = gameKey ? activeGames[gameKey] : undefined;
|
const game = gameKey ? activeGames[gameKey] : undefined;
|
||||||
if (!game || game.gameOver) return;
|
if (!game || game.gameOver) return;
|
||||||
|
|
||||||
game.gameOver = true;
|
game.gameOver = true;
|
||||||
let resultText;
|
let resultText;
|
||||||
if (winnerId === null) {
|
if (winnerId === null) {
|
||||||
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
|
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
|
||||||
resultText = 'Égalité';
|
resultText = "Égalité";
|
||||||
} else {
|
} else {
|
||||||
await eloHandler(game.p1.id, game.p2.id, game.p1.id === winnerId ? 1 : 0, game.p2.id === winnerId ? 1 : 0, title.toUpperCase());
|
await eloHandler(
|
||||||
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
|
game.p1.id,
|
||||||
resultText = `Victoire de ${winnerName}`;
|
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 === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
|
||||||
if(gameType === 'connect4') io.emit('connect4gameOver', { game, winner: winnerId });
|
if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
|
||||||
|
|
||||||
if (gameKey) {
|
if (gameKey) {
|
||||||
setTimeout(() => delete activeGames[gameKey], 1000)
|
setTimeout(() => delete activeGames[gameKey], 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Game Lifecycle & Discord Helpers ---
|
// --- Game Lifecycle & Discord Helpers ---
|
||||||
|
|
||||||
async function createGame(client, gameType) {
|
async function createGame(client, gameType) {
|
||||||
const { queue, activeGames, title } = getGameAssets(gameType);
|
const { queue, activeGames, title } = getGameAssets(gameType);
|
||||||
const p1Id = queue.shift();
|
const p1Id = queue.shift();
|
||||||
const p2Id = queue.shift();
|
const p2Id = queue.shift();
|
||||||
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
|
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
|
||||||
|
|
||||||
let lobby;
|
let lobby;
|
||||||
if (gameType === 'tictactoe') {
|
if (gameType === "tictactoe") {
|
||||||
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'X', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'O', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, sum: 1, xs: [], os: [], gameOver: false, lastmove: Date.now() };
|
lobby = {
|
||||||
} else { // connect4
|
p1: {
|
||||||
lobby = { p1: { id: p1Id, name: p1.globalName, val: 'R', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'Y', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, turn: p1Id, board: createConnect4Board(), gameOver: false, lastmove: Date.now(), winningPieces: [] };
|
id: p1Id,
|
||||||
}
|
name: p1.globalName,
|
||||||
|
val: "X",
|
||||||
|
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
|
},
|
||||||
|
p2: {
|
||||||
|
id: p2Id,
|
||||||
|
name: p2.globalName,
|
||||||
|
val: "O",
|
||||||
|
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
|
},
|
||||||
|
sum: 1,
|
||||||
|
xs: [],
|
||||||
|
os: [],
|
||||||
|
gameOver: false,
|
||||||
|
lastmove: Date.now(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// connect4
|
||||||
|
lobby = {
|
||||||
|
p1: {
|
||||||
|
id: p1Id,
|
||||||
|
name: p1.globalName,
|
||||||
|
val: "R",
|
||||||
|
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
|
},
|
||||||
|
p2: {
|
||||||
|
id: p2Id,
|
||||||
|
name: p2.globalName,
|
||||||
|
val: "Y",
|
||||||
|
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
|
},
|
||||||
|
turn: p1Id,
|
||||||
|
board: createConnect4Board(),
|
||||||
|
gameOver: false,
|
||||||
|
lastmove: Date.now(),
|
||||||
|
winningPieces: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const msgId = await updateDiscordMessage(client, lobby, title);
|
const msgId = await updateDiscordMessage(client, lobby, title);
|
||||||
lobby.msgId = msgId;
|
lobby.msgId = msgId;
|
||||||
|
|
||||||
const gameKey = `${p1Id}-${p2Id}`;
|
const gameKey = `${p1Id}-${p2Id}`;
|
||||||
activeGames[gameKey] = lobby;
|
activeGames[gameKey] = lobby;
|
||||||
|
|
||||||
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
|
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
|
||||||
await emitQueueUpdate(client, gameType);
|
await emitQueueUpdate(client, gameType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Utility Functions ---
|
// --- Utility Functions ---
|
||||||
|
|
||||||
async function refreshQueuesForUser(userId, client) {
|
async function refreshQueuesForUser(userId, client) {
|
||||||
// FIX: Mutate the array instead of reassigning it.
|
// FIX: Mutate the array instead of reassigning it.
|
||||||
let index = tictactoeQueue.indexOf(userId);
|
let index = tictactoeQueue.indexOf(userId);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
tictactoeQueue.splice(index, 1);
|
tictactoeQueue.splice(index, 1);
|
||||||
try {
|
try {
|
||||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||||
const user = await client.users.fetch(userId);
|
const user = await client.users.fetch(userId);
|
||||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
|
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||||
const updatedEmbed = new EmbedBuilder().setTitle('Tic Tac Toe').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
|
const updatedEmbed = new EmbedBuilder()
|
||||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
.setTitle("Tic Tac Toe")
|
||||||
delete queueMessagesEndpoints[userId];
|
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
|
||||||
} catch (e) {
|
.setColor(0xed4245)
|
||||||
console.error('Error updating queue message : ', e);
|
.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);
|
index = connect4Queue.indexOf(userId);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
connect4Queue.splice(index, 1);
|
connect4Queue.splice(index, 1);
|
||||||
try {
|
try {
|
||||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||||
const user = await client.users.fetch(userId);
|
const user = await client.users.fetch(userId);
|
||||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
|
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||||
const updatedEmbed = new EmbedBuilder().setTitle('Puissance 4').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
|
const updatedEmbed = new EmbedBuilder()
|
||||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
.setTitle("Puissance 4")
|
||||||
delete queueMessagesEndpoints[userId];
|
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
|
||||||
} catch (e) {
|
.setColor(0xed4245)
|
||||||
console.error('Error updating queue message : ', e);
|
.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, "tictactoe");
|
||||||
await emitQueueUpdate(client, 'connect4');
|
await emitQueueUpdate(client, "connect4");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitQueueUpdate(client, gameType) {
|
async function emitQueueUpdate(client, gameType) {
|
||||||
const { queue, activeGames } = getGameAssets(gameType);
|
const { queue, activeGames } = getGameAssets(gameType);
|
||||||
const names = await Promise.all(queue.map(async (id) => {
|
const names = await Promise.all(
|
||||||
const user = await client.users.fetch(id).catch(() => null);
|
queue.map(async (id) => {
|
||||||
return user?.globalName || user?.username;
|
const user = await client.users.fetch(id).catch(() => null);
|
||||||
}));
|
return user?.globalName || user?.username;
|
||||||
io.emit(`${gameType}queue`, { allPlayers: Object.values(activeGames), queue: names.filter(Boolean) });
|
}),
|
||||||
|
);
|
||||||
|
io.emit(`${gameType}queue`, {
|
||||||
|
allPlayers: Object.values(activeGames),
|
||||||
|
queue: names.filter(Boolean),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGameAssets(gameType) {
|
function getGameAssets(gameType) {
|
||||||
if (gameType === 'tictactoe') return { queue: tictactoeQueue, activeGames: activeTicTacToeGames, title: 'Tic Tac Toe', url: '/tic-tac-toe' };
|
if (gameType === "tictactoe")
|
||||||
if (gameType === 'connect4') return { queue: connect4Queue, activeGames: activeConnect4Games, title: 'Puissance 4', url: '/connect-4' };
|
return {
|
||||||
return { queue: [], activeGames: {} };
|
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) {
|
async function postQueueToDiscord(client, playerId, title, url) {
|
||||||
try {
|
try {
|
||||||
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
||||||
const user = await client.users.fetch(playerId);
|
const user = await client.users.fetch(playerId);
|
||||||
const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2').setTimestamp(new Date());
|
const embed = new EmbedBuilder()
|
||||||
const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel(`Jouer contre ${user.username}`).setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`).setStyle(ButtonStyle.Link));
|
.setTitle(title)
|
||||||
const msg = await generalChannel.send({ embeds: [embed], components: [row] });
|
.setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`)
|
||||||
queueMessagesEndpoints[playerId] = msg.id
|
.setColor("#5865F2")
|
||||||
} catch (e) { console.error(`Failed to post queue message for ${title}:`, e); }
|
.setTimestamp(new Date());
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setLabel(`Jouer contre ${user.username}`)
|
||||||
|
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`)
|
||||||
|
.setStyle(ButtonStyle.Link),
|
||||||
|
);
|
||||||
|
const msg = await generalChannel.send({
|
||||||
|
embeds: [embed],
|
||||||
|
components: [row],
|
||||||
|
});
|
||||||
|
queueMessagesEndpoints[playerId] = msg.id;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to post queue message for ${title}:`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDiscordMessage(client, game, title, resultText = '') {
|
async function updateDiscordMessage(client, game, title, resultText = "") {
|
||||||
const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
|
const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null);
|
||||||
if (!channel) return null;
|
if (!channel) return null;
|
||||||
|
|
||||||
let description;
|
let description;
|
||||||
if (title === 'Tic Tac Toe') {
|
if (title === "Tic Tac Toe") {
|
||||||
let gridText = '';
|
let gridText = "";
|
||||||
for (let i = 1; i <= 9; i++) { gridText += game.xs.includes(i) ? '❌' : game.os.includes(i) ? '⭕' : '🟦'; if (i % 3 === 0) gridText += '\n'; }
|
for (let i = 1; i <= 9; i++) {
|
||||||
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
|
gridText += game.xs.includes(i) ? "❌" : game.os.includes(i) ? "⭕" : "🟦";
|
||||||
} else {
|
if (i % 3 === 0) gridText += "\n";
|
||||||
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
|
}
|
||||||
}
|
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
|
||||||
if (resultText) description += `\n### ${resultText}`;
|
} 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 {
|
try {
|
||||||
if (game.msgId) {
|
if (game.msgId) {
|
||||||
const message = await channel.messages.fetch(game.msgId);
|
const message = await channel.messages.fetch(game.msgId);
|
||||||
await message.edit({ embeds: [embed] });
|
await message.edit({ embeds: [embed] });
|
||||||
return game.msgId;
|
return game.msgId;
|
||||||
} else {
|
} else {
|
||||||
const message = await channel.send({ embeds: [embed] });
|
const message = await channel.send({ embeds: [embed] });
|
||||||
return message.id;
|
return message.id;
|
||||||
}
|
}
|
||||||
} catch (e) { return null; }
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupStaleGames() {
|
function cleanupStaleGames() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const STALE_TIMEOUT = 30 * 60 * 1000;
|
const STALE_TIMEOUT = 30 * 60 * 1000;
|
||||||
const cleanup = (games, name) => {
|
const cleanup = (games, name) => {
|
||||||
Object.keys(games).forEach(key => {
|
Object.keys(games).forEach((key) => {
|
||||||
if (now - games[key].lastmove > STALE_TIMEOUT) {
|
if (now - games[key].lastmove > STALE_TIMEOUT) {
|
||||||
console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
|
console.log(`[Cleanup] Removing stale ${name} game: ${key}`);
|
||||||
delete games[key];
|
delete games[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
cleanup(activeTicTacToeGames, 'TicTacToe');
|
cleanup(activeTicTacToeGames, "TicTacToe");
|
||||||
cleanup(activeConnect4Games, 'Connect4');
|
cleanup(activeConnect4Games, "Connect4");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EMITS */
|
/* EMITS */
|
||||||
export async function socketEmit(event, data) {
|
export async function socketEmit(event, data) {
|
||||||
io.emit(event, data);
|
io.emit(event, data);
|
||||||
}
|
}
|
||||||
export async function emitDataUpdated(data) {
|
export async function emitDataUpdated(data) {
|
||||||
io.emit('data-updated', data);
|
io.emit("data-updated", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function emitPokerUpdate(data) {
|
export async function emitPokerUpdate(data) {
|
||||||
io.emit('poker-update', data);
|
io.emit("poker-update", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function emitPokerToast(data) {
|
export async function emitPokerToast(data) {
|
||||||
io.emit('poker-toast', data);
|
io.emit("poker-toast", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
|
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
|
||||||
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
||||||
|
|
||||||
export const emitSolitaireUpdate = (userId, moves) => io.emit('solitaire:update', {userId, moves});
|
export const emitSolitaireUpdate = (userId, moves) => io.emit("solitaire:update", { userId, moves });
|
||||||
|
|||||||
270
src/utils/ai.js
270
src/utils/ai.js
@@ -1,27 +1,26 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import {GoogleGenAI} from "@google/genai";
|
import { GoogleGenAI } from "@google/genai";
|
||||||
import {Mistral} from '@mistralai/mistralai';
|
import { Mistral } from "@mistralai/mistralai";
|
||||||
|
|
||||||
// --- AI Client Initialization ---
|
// --- AI Client Initialization ---
|
||||||
// Initialize clients for each AI service. This is done once when the module is loaded.
|
// Initialize clients for each AI service. This is done once when the module is loaded.
|
||||||
|
|
||||||
let openai;
|
let openai;
|
||||||
if (process.env.OPENAI_API_KEY) {
|
if (process.env.OPENAI_API_KEY) {
|
||||||
openai = new OpenAI();
|
openai = new OpenAI();
|
||||||
}
|
}
|
||||||
|
|
||||||
let gemini;
|
let gemini;
|
||||||
if (process.env.GEMINI_KEY) {
|
if (process.env.GEMINI_KEY) {
|
||||||
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY})
|
gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY });
|
||||||
}
|
}
|
||||||
|
|
||||||
let mistral;
|
let mistral;
|
||||||
if (process.env.MISTRAL_KEY) {
|
if (process.env.MISTRAL_KEY) {
|
||||||
mistral = new Mistral({apiKey: process.env.MISTRAL_KEY});
|
mistral = new Mistral({ apiKey: process.env.MISTRAL_KEY });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a response from the configured AI model.
|
* Gets a response from the configured AI model.
|
||||||
* It dynamically chooses the provider based on the MODEL environment variable.
|
* It dynamically chooses the provider based on the MODEL environment variable.
|
||||||
@@ -29,175 +28,180 @@ if (process.env.MISTRAL_KEY) {
|
|||||||
* @returns {Promise<string>} The content of the AI's response message.
|
* @returns {Promise<string>} The content of the AI's response message.
|
||||||
*/
|
*/
|
||||||
export async function gork(messageHistory) {
|
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 {
|
try {
|
||||||
// --- OpenAI Provider ---
|
// --- OpenAI Provider ---
|
||||||
if (modelProvider === 'OpenAI' && openai) {
|
if (modelProvider === "OpenAI" && openai) {
|
||||||
const completion = await openai.chat.completions.create({
|
const completion = await openai.chat.completions.create({
|
||||||
model: "gpt-5", // Using a modern, cost-effective model
|
model: "gpt-5", // Using a modern, cost-effective model
|
||||||
reasoning_effort: "low",
|
reasoning_effort: "low",
|
||||||
messages: messageHistory,
|
messages: messageHistory,
|
||||||
});
|
});
|
||||||
return completion.choices[0].message.content;
|
return completion.choices[0].message.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Google Gemini Provider ---
|
// --- Google Gemini Provider ---
|
||||||
else if (modelProvider === 'Gemini' && gemini) {
|
else if (modelProvider === "Gemini" && gemini) {
|
||||||
// Gemini requires a slightly different history format.
|
// Gemini requires a slightly different history format.
|
||||||
const contents = messageHistory.map(msg => ({
|
const contents = messageHistory.map((msg) => ({
|
||||||
role: msg.role === 'assistant' ? 'model' : msg.role, // Gemini uses 'model' for assistant role
|
role: msg.role === "assistant" ? "model" : msg.role, // Gemini uses 'model' for assistant role
|
||||||
parts: [{ text: msg.content }],
|
parts: [{ text: msg.content }],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// The last message should not be from the model
|
// The last message should not be from the model
|
||||||
if (contents[contents.length - 1].role === 'model') {
|
if (contents[contents.length - 1].role === "model") {
|
||||||
contents.pop();
|
contents.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await gemini.generateContent({ contents });
|
const result = await gemini.generateContent({ contents });
|
||||||
const response = await result.response;
|
const response = await result.response;
|
||||||
return response.text();
|
return response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mistral Provider ---
|
// --- Mistral Provider ---
|
||||||
else if (modelProvider === 'Mistral' && mistral) {
|
else if (modelProvider === "Mistral" && mistral) {
|
||||||
const chatResponse = await mistral.chat({
|
const chatResponse = await mistral.chat({
|
||||||
model: 'mistral-large-latest',
|
model: "mistral-large-latest",
|
||||||
messages: messageHistory,
|
messages: messageHistory,
|
||||||
});
|
});
|
||||||
return chatResponse.choices[0].message.content;
|
return chatResponse.choices[0].message.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fallback Case ---
|
// --- Fallback Case ---
|
||||||
else {
|
else {
|
||||||
console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`);
|
console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`);
|
||||||
return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur.";
|
return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur.";
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
console.error(`[AI] Error with ${modelProvider} API:`, error);
|
console.error(`[AI] Error with ${modelProvider} API:`, error);
|
||||||
return "Oups, une erreur est survenue en contactant le service d'IA.";
|
return "Oups, une erreur est survenue en contactant le service d'IA.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || '100', 10);
|
export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || "100", 10);
|
||||||
export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || '3', 10);
|
export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || "3", 10);
|
||||||
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || 'true') === 'true';
|
export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || "true") === "true";
|
||||||
|
|
||||||
export const stripMentionsOfBot = (text, botId) =>
|
export const stripMentionsOfBot = (text, botId) => text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
|
||||||
text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim();
|
|
||||||
|
|
||||||
export const sanitize = (s) =>
|
export const sanitize = (s) =>
|
||||||
(s || '')
|
(s || "")
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, " ")
|
||||||
.replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels
|
.replace(/```/g, "ʼʼʼ") // éviter de casser des fences éventuels
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
export const shortTs = (d) => new Date(d).toISOString(); // compact et triable
|
export const shortTs = (d) => new Date(d).toISOString(); // compact et triable
|
||||||
|
|
||||||
export function buildParticipantsMap(messages) {
|
export function buildParticipantsMap(messages) {
|
||||||
const map = {};
|
const map = {};
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const id = m.author.id;
|
const id = m.author.id;
|
||||||
if (!map[id]) {
|
if (!map[id]) {
|
||||||
map[id] = {
|
map[id] = {
|
||||||
id,
|
id,
|
||||||
username: m.author.username,
|
username: m.author.username,
|
||||||
globalName: m.author.globalName || null,
|
globalName: m.author.globalName || null,
|
||||||
isBot: !!m.author.bot,
|
isBot: !!m.author.bot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTranscript(messages, botId) {
|
export function buildTranscript(messages, botId) {
|
||||||
// Oldest -> newest, JSONL compact, une ligne par message pertinent
|
// Oldest -> newest, JSONL compact, une ligne par message pertinent
|
||||||
const lines = [];
|
const lines = [];
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
const content = sanitize(m.content);
|
const content = sanitize(m.content);
|
||||||
const atts = Array.from(m.attachments?.values?.() || []);
|
const atts = Array.from(m.attachments?.values?.() || []);
|
||||||
if (!content && atts.length === 0) continue;
|
if (!content && atts.length === 0) continue;
|
||||||
|
|
||||||
const attMeta = atts.length
|
const attMeta = atts.length
|
||||||
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({
|
? atts.slice(0, MAX_ATTS_PER_MESSAGE).map((a) => ({
|
||||||
id: a.id,
|
id: a.id,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
type: a.contentType || 'application/octet-stream',
|
type: a.contentType || "application/octet-stream",
|
||||||
size: a.size,
|
size: a.size,
|
||||||
isImage: !!(a.contentType && a.contentType.startsWith('image/')),
|
isImage: !!(a.contentType && a.contentType.startsWith("image/")),
|
||||||
width: a.width || undefined,
|
width: a.width || undefined,
|
||||||
height: a.height || undefined,
|
height: a.height || undefined,
|
||||||
spoiler: typeof a.spoiler === 'boolean' ? a.spoiler : false,
|
spoiler: typeof a.spoiler === "boolean" ? a.spoiler : false,
|
||||||
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
|
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut
|
||||||
}))
|
}))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const line = {
|
const line = {
|
||||||
t: shortTs(m.createdTimestamp || Date.now()),
|
t: shortTs(m.createdTimestamp || Date.now()),
|
||||||
id: m.author.id,
|
id: m.author.id,
|
||||||
nick: m.member?.nickname || m.author.globalName || m.author.username,
|
nick: m.member?.nickname || m.author.globalName || m.author.username,
|
||||||
isBot: !!m.author.bot,
|
isBot: !!m.author.bot,
|
||||||
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''),
|
mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ""),
|
||||||
replyTo: m.reference?.messageId || null,
|
replyTo: m.reference?.messageId || null,
|
||||||
content,
|
content,
|
||||||
attachments: attMeta,
|
attachments: attMeta,
|
||||||
};
|
};
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
return lines.map(l => JSON.stringify(l)).join('\n');
|
return lines.map((l) => JSON.stringify(l)).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAiMessages({
|
export function buildAiMessages({
|
||||||
botId,
|
botId,
|
||||||
botName = 'FlopoBot',
|
botName = "FlopoBot",
|
||||||
invokerId,
|
invokerId,
|
||||||
invokerName,
|
invokerName,
|
||||||
requestText,
|
requestText,
|
||||||
transcript,
|
transcript,
|
||||||
participants,
|
participants,
|
||||||
repliedUserId,
|
repliedUserId,
|
||||||
invokerAttachments = [],
|
invokerAttachments = [],
|
||||||
}) {
|
}) {
|
||||||
const system = {
|
const system = {
|
||||||
role: 'system',
|
role: "system",
|
||||||
content:
|
content: `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
|
||||||
`Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote.
|
|
||||||
Règles de sortie:
|
Règles de sortie:
|
||||||
- Réponds en français, en 1–3 phrases.
|
- Réponds en français, en 1–3 phrases.
|
||||||
- Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif.
|
- Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif.
|
||||||
- Pas de "Untel a dit…", pas de longs préambules.
|
- Pas de "Untel a dit…", pas de longs préambules.
|
||||||
- Utilise <@ID> pour mentionner quelqu'un.
|
- 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").`,
|
- 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
|
const attLines = invokerAttachments.length
|
||||||
? invokerAttachments.map(a => `- ${a.name} (${a.type || 'type inconnu'}, ${a.size ?? '?'} o${a.isImage ? ', image' : ''})`).join('\n')
|
? invokerAttachments
|
||||||
: '';
|
.map((a) => `- ${a.name} (${a.type || "type inconnu"}, ${a.size ?? "?"} o${a.isImage ? ", image" : ""})`)
|
||||||
|
.join("\n")
|
||||||
|
: "";
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: 'user',
|
role: "user",
|
||||||
content:
|
content: `Tâche: répondre brièvement à <@${invokerId}>.
|
||||||
`Tâche: répondre brièvement à <@${invokerId}>.
|
|
||||||
|
|
||||||
Message de <@${invokerId}> (${invokerName || 'inconnu'}):
|
Message de <@${invokerId}> (${invokerName || "inconnu"}):
|
||||||
"""
|
"""
|
||||||
${requestText}
|
${requestText}
|
||||||
"""
|
"""
|
||||||
${invokerAttachments.length ? `Pièces jointes du message:
|
${
|
||||||
|
invokerAttachments.length
|
||||||
|
? `Pièces jointes du message:
|
||||||
${attLines}
|
${attLines}
|
||||||
` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''}
|
`
|
||||||
|
: ""
|
||||||
|
}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ""}
|
||||||
|
|
||||||
Participants (id -> nom):
|
Participants (id -> nom):
|
||||||
${Object.values(participants).map(p => `- ${p.id} -> ${p.globalName || p.username}`).join('\n')}
|
${Object.values(participants)
|
||||||
|
.map((p) => `- ${p.id} -> ${p.globalName || p.username}`)
|
||||||
|
.join("\n")}
|
||||||
|
|
||||||
Contexte (transcript JSONL; à utiliser seulement si utile):
|
Contexte (transcript JSONL; à utiliser seulement si utile):
|
||||||
\`\`\`jsonl
|
\`\`\`jsonl
|
||||||
${transcript}
|
${transcript}
|
||||||
\`\`\``,
|
\`\`\``,
|
||||||
};
|
};
|
||||||
|
|
||||||
return [system, user];
|
return [system, user];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
export const roles = {
|
export const roles = {
|
||||||
erynie_1: {
|
erynie_1: {
|
||||||
name: 'Erinye',
|
name: "Erinye",
|
||||||
subtitle: 'Mégère, la haine',
|
subtitle: "Mégère, la haine",
|
||||||
descr: '',
|
descr: "",
|
||||||
powers: {
|
powers: {
|
||||||
double_vote: {
|
double_vote: {
|
||||||
descr: 'Les Erinyes peuvent tuer une deuxième personne (1 seule fois).',
|
descr: "Les Erinyes peuvent tuer une deuxième personne (1 seule fois).",
|
||||||
charges: 1,
|
charges: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
passive: {},
|
passive: {},
|
||||||
team: 'Erinyes',
|
team: "Erinyes",
|
||||||
},
|
},
|
||||||
erynie_2: {
|
erynie_2: {
|
||||||
name: 'Erinye',
|
name: "Erinye",
|
||||||
subtitle: 'Tisiphone, la vengeance',
|
subtitle: "Tisiphone, la vengeance",
|
||||||
descr: '',
|
descr: "",
|
||||||
powers: {
|
powers: {
|
||||||
one_shot: {
|
one_shot: {
|
||||||
descr: 'Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).',
|
descr: "Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).",
|
||||||
charges: 1,
|
charges: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
passive: {},
|
passive: {},
|
||||||
team: 'Erinyes',
|
team: "Erinyes",
|
||||||
},
|
},
|
||||||
erynie_3: {
|
erynie_3: {
|
||||||
name: 'Erinye',
|
name: "Erinye",
|
||||||
subtitle: 'Alecto, l\'implacable',
|
subtitle: "Alecto, l'implacable",
|
||||||
descr: '',
|
descr: "",
|
||||||
powers: {
|
powers: {
|
||||||
silence: {
|
silence: {
|
||||||
descr: 'Empêche l\'utilisation du pouvoir de quelqu\'un pour le prochain tour.',
|
descr: "Empêche l'utilisation du pouvoir de quelqu'un pour le prochain tour.",
|
||||||
charges: 999,
|
charges: 999,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
passive: {
|
passive: {
|
||||||
descr: 'Voit quels pouvoirs ont été utilisés.',
|
descr: "Voit quels pouvoirs ont été utilisés.",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
team: 'Erinyes',
|
team: "Erinyes",
|
||||||
},
|
},
|
||||||
narcisse: {
|
narcisse: {
|
||||||
name: 'Narcisse',
|
name: "Narcisse",
|
||||||
subtitle: '',
|
subtitle: "",
|
||||||
descr: '',
|
descr: "",
|
||||||
powers: {},
|
powers: {},
|
||||||
passive: {
|
passive: {
|
||||||
descr: 'S\'il devient maire ...',
|
descr: "S'il devient maire ...",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
charon: {
|
charon: {
|
||||||
name: 'Charon',
|
name: "Charon",
|
||||||
subtitle: 'Sorcier',
|
subtitle: "Sorcier",
|
||||||
descr: 'C\'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.',
|
descr: "C'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.",
|
||||||
powers: {
|
powers: {
|
||||||
revive: {
|
revive: {
|
||||||
descr: 'Refuser de faire traverser le Styx (sauver quelqu\'un)',
|
descr: "Refuser de faire traverser le Styx (sauver quelqu'un)",
|
||||||
charges: 1,
|
charges: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
kill: {
|
kill: {
|
||||||
descr: 'Traverser le Styx (tuer quelqu\'un)',
|
descr: "Traverser le Styx (tuer quelqu'un)",
|
||||||
charges: 1,
|
charges: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
//...
|
//...
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import cron from 'node-cron';
|
import cron from "node-cron";
|
||||||
import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
|
|
||||||
|
|
||||||
// --- Local Imports ---
|
// --- Local Imports ---
|
||||||
import { getValorantSkins, getSkinTiers } from '../api/valorant.js';
|
import { getSkinTiers, getValorantSkins } from "../api/valorant.js";
|
||||||
import { DiscordRequest } from '../api/discord.js';
|
import { DiscordRequest } from "../api/discord.js";
|
||||||
import { initTodaysSOTD } from '../game/points.js';
|
import { initTodaysSOTD } from "../game/points.js";
|
||||||
import {
|
import {
|
||||||
insertManyUsers, insertManySkins, resetDailyReward,
|
getAllAkhys,
|
||||||
pruneOldLogs, getAllUsers as dbGetAllUsers, getSOTD, getUser, getAllUsers, insertUser, stmtUsers,
|
getAllUsers,
|
||||||
} from '../database/index.js';
|
insertManySkins,
|
||||||
import { activeInventories, activeSearchs, activePredis, pokerRooms, skins } from '../game/state.js';
|
insertUser,
|
||||||
|
resetDailyReward,
|
||||||
|
updateUserAvatar,
|
||||||
|
} from "../database/index.js";
|
||||||
|
import { activeInventories, activeSearchs, skins } from "../game/state.js";
|
||||||
|
|
||||||
export async function InstallGlobalCommands(appId, commands) {
|
export async function InstallGlobalCommands(appId, commands) {
|
||||||
// API endpoint to overwrite global commands
|
// API endpoint to overwrite global commands
|
||||||
const endpoint = `applications/${appId}/commands`;
|
const endpoint = `applications/${appId}/commands`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
|
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
|
||||||
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
|
await DiscordRequest(endpoint, { method: "PUT", body: commands });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Data Fetching & Initialization ---
|
// --- Data Fetching & Initialization ---
|
||||||
@@ -32,74 +35,74 @@ export async function InstallGlobalCommands(appId, commands) {
|
|||||||
* @param {object} client - The Discord.js client instance.
|
* @param {object} client - The Discord.js client instance.
|
||||||
*/
|
*/
|
||||||
export async function getAkhys(client) {
|
export async function getAkhys(client) {
|
||||||
try {
|
try {
|
||||||
// 1. Fetch Discord Members
|
// 1. Fetch Discord Members
|
||||||
const initial_akhys = getAllUsers.all().length;
|
const initial_akhys = getAllUsers.all().length;
|
||||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||||
const members = await guild.members.fetch();
|
const members = await guild.members.fetch();
|
||||||
const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
|
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
|
||||||
|
|
||||||
|
const usersToInsert = akhys.map((akhy) => ({
|
||||||
|
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 => ({
|
if (usersToInsert.length > 0) {
|
||||||
id: akhy.user.id,
|
usersToInsert.forEach((user) => {
|
||||||
username: akhy.user.username,
|
try {
|
||||||
globalName: akhy.user.globalName,
|
insertUser.run(user);
|
||||||
warned: 0,
|
} catch (err) {}
|
||||||
warns: 0,
|
});
|
||||||
allTimeWarns: 0,
|
}
|
||||||
totalRequests: 0,
|
|
||||||
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
|
|
||||||
isAkhy: 1
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (usersToInsert.length > 0) {
|
const new_akhys = getAllUsers.all().length;
|
||||||
usersToInsert.forEach(user => {
|
const diff = new_akhys - initial_akhys;
|
||||||
try { insertUser.run(user) } catch (err) {}
|
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;
|
// 2. Fetch Valorant Skins
|
||||||
const diff = new_akhys - initial_akhys
|
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})`);
|
|
||||||
|
|
||||||
// 2. Fetch Valorant Skins
|
// Clear and rebuild the in-memory skin cache
|
||||||
const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]);
|
skins.length = 0;
|
||||||
|
fetchedSkins.forEach((skin) => skins.push(skin));
|
||||||
|
|
||||||
// Clear and rebuild the in-memory skin cache
|
const skinsToInsert = fetchedSkins
|
||||||
skins.length = 0;
|
.filter((skin) => skin.contentTierUuid)
|
||||||
fetchedSkins.forEach(skin => skins.push(skin));
|
.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),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const skinsToInsert = fetchedSkins
|
if (skinsToInsert.length > 0) {
|
||||||
.filter(skin => skin.contentTierUuid)
|
insertManySkins(skinsToInsert);
|
||||||
.map(skin => {
|
}
|
||||||
const tier = fetchedTiers.find(t => t.uuid === skin.contentTierUuid) || {};
|
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
|
||||||
const basePrice = calculateBasePrice(skin, tier.rank);
|
} catch (err) {
|
||||||
return {
|
console.error("Error during initial data sync (getAkhys):", err);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Cron Jobs / Scheduled Tasks ---
|
// --- Cron Jobs / Scheduled Tasks ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,72 +111,87 @@ export async function getAkhys(client) {
|
|||||||
* @param {object} io - The Socket.IO server instance.
|
* @param {object} io - The Socket.IO server instance.
|
||||||
*/
|
*/
|
||||||
export function setupCronJobs(client, io) {
|
export function setupCronJobs(client, io) {
|
||||||
// Every 10 minutes: Clean up expired interactive sessions
|
// Every 10 minutes: Clean up expired interactive sessions
|
||||||
cron.schedule('*/10 * * * *', () => {
|
cron.schedule("*/10 * * * *", () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const cleanup = (sessions, name) => {
|
const cleanup = (sessions, name) => {
|
||||||
let cleanedCount = 0;
|
let cleanedCount = 0;
|
||||||
for (const id in sessions) {
|
for (const id in sessions) {
|
||||||
if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) {
|
if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) {
|
||||||
delete sessions[id];
|
delete sessions[id];
|
||||||
cleanedCount++;
|
cleanedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
|
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
cleanup(activeInventories, 'inventory');
|
cleanup(activeInventories, "inventory");
|
||||||
cleanup(activeSearchs, 'search');
|
cleanup(activeSearchs, "search");
|
||||||
|
|
||||||
// Cleanup for predis and poker rooms...
|
// TODO: Cleanup for predis and poker rooms...
|
||||||
// ...
|
// ...
|
||||||
});
|
});
|
||||||
|
|
||||||
// Daily at midnight: Reset daily rewards and init SOTD
|
// Daily at midnight: Reset daily rewards and init SOTD
|
||||||
cron.schedule('0 0 * * *', async () => {
|
cron.schedule("0 0 * * *", async () => {
|
||||||
console.log('[Cron] Running daily midnight tasks...');
|
console.log("[Cron] Running daily midnight tasks...");
|
||||||
try {
|
try {
|
||||||
resetDailyReward.run();
|
resetDailyReward.run();
|
||||||
console.log('[Cron] Daily rewards have been reset for all users.');
|
console.log("[Cron] Daily rewards have been reset for all users.");
|
||||||
//if (!getSOTD.get()) {
|
//if (!getSOTD.get()) {
|
||||||
initTodaysSOTD();
|
initTodaysSOTD();
|
||||||
//}
|
//}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Cron] Error during daily reset:', e);
|
console.error("[Cron] Error during daily reset:", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Daily at 7 AM: Re-sync users and skins
|
// Daily at 7 AM: Re-sync users and skins
|
||||||
cron.schedule('0 7 * * *', async () => {
|
cron.schedule("0 7 * * *", async () => {
|
||||||
console.log('[Cron] Running daily 7 AM data sync...');
|
console.log("[Cron] Running daily 7 AM data sync...");
|
||||||
await getAkhys(client);
|
await getAkhys(client);
|
||||||
});
|
try {
|
||||||
|
const akhys = getAllAkhys.all();
|
||||||
|
for (const akhy of akhys) {
|
||||||
|
const user = await client.users.cache.get(akhy.id);
|
||||||
|
try {
|
||||||
|
updateUserAvatar.run({
|
||||||
|
id: akhy.id,
|
||||||
|
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Cron] Error during daily avatar update:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Formatting Helpers ---
|
// --- Formatting Helpers ---
|
||||||
|
|
||||||
export function capitalize(str) {
|
export function capitalize(str) {
|
||||||
if (typeof str !== 'string' || str.length === 0) return '';
|
if (typeof str !== "string" || str.length === 0) return "";
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTime(seconds) {
|
export function formatTime(seconds) {
|
||||||
const d = Math.floor(seconds / (3600*24));
|
const d = Math.floor(seconds / (3600 * 24));
|
||||||
const h = Math.floor(seconds % (3600*24) / 3600);
|
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
const m = Math.floor(seconds % 3600 / 60);
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (d > 0) parts.push(`**${d}** jour${d > 1 ? 's' : ''}`);
|
if (d > 0) parts.push(`**${d}** jour${d > 1 ? "s" : ""}`);
|
||||||
if (h > 0) parts.push(`**${h}** heure${h > 1 ? 's' : ''}`);
|
if (h > 0) parts.push(`**${h}** heure${h > 1 ? "s" : ""}`);
|
||||||
if (m > 0) parts.push(`**${m}** minute${m > 1 ? 's' : ''}`);
|
if (m > 0) parts.push(`**${m}** minute${m > 1 ? "s" : ""}`);
|
||||||
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? 's' : ''}`);
|
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? "s" : ""}`);
|
||||||
|
|
||||||
return parts.join(', ').replace(/,([^,]*)$/, ' et$1');
|
return parts.join(", ").replace(/,([^,]*)$/, " et$1");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- External API Helpers ---
|
// --- External API Helpers ---
|
||||||
@@ -182,15 +200,15 @@ export function formatTime(seconds) {
|
|||||||
* Fetches user data from the "APO" service.
|
* Fetches user data from the "APO" service.
|
||||||
*/
|
*/
|
||||||
export async function getAPOUsers() {
|
export async function getAPOUsers() {
|
||||||
const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`;
|
const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(fetchUrl);
|
const response = await fetch(fetchUrl);
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching APO users:', error);
|
console.error("Error fetching APO users:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,112 +217,124 @@ export async function getAPOUsers() {
|
|||||||
* @param {number} amount - The amount to "buy".
|
* @param {number} amount - The amount to "buy".
|
||||||
*/
|
*/
|
||||||
export async function postAPOBuy(userId, amount) {
|
export async function postAPOBuy(userId, amount) {
|
||||||
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
|
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
|
||||||
return fetch(fetchUrl, { method: 'POST' });
|
return fetch(fetchUrl, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Miscellaneous Helpers ---
|
// --- Miscellaneous Helpers ---
|
||||||
|
|
||||||
export async function getOnlineUsersWithRole(guild, roleId) {
|
export async function getOnlineUsersWithRole(guild, roleId) {
|
||||||
if (!guild || !roleId) return new Map();
|
if (!guild || !roleId) return new Map();
|
||||||
try {
|
try {
|
||||||
const members = await guild.members.fetch();
|
const members = await guild.members.fetch();
|
||||||
return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.presence?.status !== undefined && m.roles.cache.has(roleId));
|
return members.filter(
|
||||||
} catch (err) {
|
(m) =>
|
||||||
console.error('Error fetching online members with role:', err);
|
!m.user.bot &&
|
||||||
return new Map();
|
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) {
|
export function getRandomEmoji(list = 0) {
|
||||||
const emojiLists = [
|
const emojiLists = [
|
||||||
['😭','😄','😌','🤓','😎','😤','🤖','😶🌫️','🌏','📸','💿','👋','🌊','✨'],
|
["😭", "😄", "😌", "🤓", "😎", "😤", "🤖", "😶🌫️", "🌏", "📸", "💿", "👋", "🌊", "✨"],
|
||||||
['<:CAUGHT:1323810730155446322>', '<:hinhinhin:1072510144933531758>', '<:o7:1290773422451986533>', '<:zhok:1115221772623683686>', '<:nice:1154049521110765759>', '<:nerd:1087658195603951666>', '<:peepSelfie:1072508131839594597>'],
|
[
|
||||||
];
|
"<:CAUGHT:1323810730155446322>",
|
||||||
const selectedList = emojiLists[list] || [''];
|
"<:hinhinhin:1072510144933531758>",
|
||||||
return selectedList[Math.floor(Math.random() * selectedList.length)];
|
"<: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) {
|
export function formatAmount(amount) {
|
||||||
if (amount >= 1000000000) {
|
if (amount >= 1000000000) {
|
||||||
amount /= 1000000000
|
amount /= 1000000000;
|
||||||
return (
|
return (
|
||||||
amount
|
amount
|
||||||
.toFixed(2)
|
.toFixed(2)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'Md'
|
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "Md"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
if (amount >= 1000000) {
|
if (amount >= 1000000) {
|
||||||
amount /= 1000000
|
amount /= 1000000;
|
||||||
return (
|
return (
|
||||||
amount
|
amount
|
||||||
.toFixed(2)
|
.toFixed(2)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'M'
|
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "M"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
if (amount >= 10000) {
|
if (amount >= 10000) {
|
||||||
amount /= 1000
|
amount /= 1000;
|
||||||
return (
|
return (
|
||||||
amount
|
amount
|
||||||
.toFixed(2)
|
.toFixed(2)
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'K'
|
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "K"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
|
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Private Helpers ---
|
// --- Private Helpers ---
|
||||||
|
|
||||||
export function calculateBasePrice(skin, tierRank) {
|
export function calculateBasePrice(skin, tierRank) {
|
||||||
const name = skin.displayName.toLowerCase();
|
const name = skin.displayName.toLowerCase();
|
||||||
let price = 6000; // Default for melee
|
let price = 6000; // Default for melee
|
||||||
if (name.includes('classic')) price = 150;
|
if (name.includes("classic")) price = 150;
|
||||||
else if (name.includes('shorty')) price = 300;
|
else if (name.includes("shorty")) price = 300;
|
||||||
else if (name.includes('frenzy')) price = 450;
|
else if (name.includes("frenzy")) price = 450;
|
||||||
else if (name.includes('ghost')) price = 500;
|
else if (name.includes("ghost")) price = 500;
|
||||||
else if (name.includes('sheriff')) price = 800;
|
else if (name.includes("sheriff")) price = 800;
|
||||||
else if (name.includes('stinger')) price = 1000;
|
else if (name.includes("stinger")) price = 1000;
|
||||||
else if (name.includes('spectre')) price = 1600;
|
else if (name.includes("spectre")) price = 1600;
|
||||||
else if (name.includes('bucky')) price = 900;
|
else if (name.includes("bucky")) price = 900;
|
||||||
else if (name.includes('judge')) price = 1500;
|
else if (name.includes("judge")) price = 1500;
|
||||||
else if (name.includes('bulldog')) price = 2100;
|
else if (name.includes("bulldog")) price = 2100;
|
||||||
else if (name.includes('guardian')) price = 2700
|
else if (name.includes("guardian")) price = 2700;
|
||||||
else if (name.includes('vandal') || name.includes('phantom')) price = 2900;
|
else if (name.includes("vandal") || name.includes("phantom")) price = 2900;
|
||||||
else if (name.includes('marshal')) price = 950;
|
else if (name.includes("marshal")) price = 950;
|
||||||
else if (name.includes('outlaw')) price = 2400;
|
else if (name.includes("outlaw")) price = 2400;
|
||||||
else if (name.includes('operator')) price = 4500;
|
else if (name.includes("operator")) price = 4500;
|
||||||
else if (name.includes('ares')) price = 1700;
|
else if (name.includes("ares")) price = 1700;
|
||||||
else if (name.includes('odin')) price = 3200;
|
else if (name.includes("odin")) price = 3200;
|
||||||
|
|
||||||
price *= (1 + (tierRank || 0));
|
price *= 1 + (tierRank || 0);
|
||||||
if (name.includes('vct')) price *= 1.25;
|
if (name.includes("vct")) price *= 1.25;
|
||||||
if (name.includes('champions')) price *= 2;
|
if (name.includes("champions")) price *= 2;
|
||||||
|
|
||||||
return price / 124;
|
return price / 124;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateMaxPrice(basePrice, skin) {
|
export function calculateMaxPrice(basePrice, skin) {
|
||||||
let res = basePrice;
|
let res = basePrice;
|
||||||
res *= (1 + (skin.levels.length / Math.max(skin.levels.length, 2)));
|
res *= 1 + skin.levels.length / Math.max(skin.levels.length, 2);
|
||||||
res *= (1 + (skin.chromas.length / 4));
|
res *= 1 + skin.chromas.length / 4;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTierText(rank, displayName) {
|
function formatTierText(rank, displayName) {
|
||||||
const tiers = {
|
const tiers = {
|
||||||
0: '**<:select:1362964319498670222> Select**',
|
0: "**<:select:1362964319498670222> Select**",
|
||||||
1: '**<:deluxe:1362964308094488797> Deluxe**',
|
1: "**<:deluxe:1362964308094488797> Deluxe**",
|
||||||
2: '**<:premium:1362964330349330703> Premium**',
|
2: "**<:premium:1362964330349330703> Premium**",
|
||||||
3: '**<:exclusive:1362964427556651098> Exclusive**',
|
3: "**<:exclusive:1362964427556651098> Exclusive**",
|
||||||
4: '**<:ultra:1362964339685986314> Ultra**',
|
4: "**<:ultra:1362964339685986314> Ultra**",
|
||||||
};
|
};
|
||||||
let res = tiers[rank] || 'Pas de tier';
|
let res = tiers[rank] || "Pas de tier";
|
||||||
if (displayName.includes('VCT')) res += ' | Esports';
|
if (displayName.includes("VCT")) res += " | Esports";
|
||||||
if (displayName.toLowerCase().includes('champions')) res += ' | Champions';
|
if (displayName.toLowerCase().includes("champions")) res += " | Champions";
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user