From 7d706b4d799702270926d3b4b747911b69648b64 Mon Sep 17 00:00:00 2001 From: Arthur Puechberty Date: Sat, 17 Jan 2026 18:52:16 +0100 Subject: [PATCH] add economi system --- app/commands/adminAddXp.js | 224 +++++++++++++++++++++++++++ app/commands/adminEcoAdd.js | 108 +++++++++++++ app/commands/adminEcoRemove.js | 108 +++++++++++++ app/commands/adminRemoveXp.js | 198 +++++++++++++++++++++++ app/commands/adminSetXp.js | 187 ++++++++++++++++++++++ app/commands/balance.js | 99 ++++++++++++ app/commands/crime.js | 138 +++++++++++++++++ app/commands/daily.js | 97 ++++++++++++ app/commands/deposit.js | 118 ++++++++++++++ app/commands/pay.js | 147 ++++++++++++++++++ app/commands/richest.js | 82 ++++++++++ app/commands/withdraw.js | 114 ++++++++++++++ app/commands/work.js | 114 ++++++++++++++ app/db.js | 50 ++++++ app/events/executeCommands/prefix.js | 22 +++ app/events/executeCommands/slash.js | 19 ++- app/fonctions/addCommand.js | 24 ++- app/public/guild.html | 98 ++++++++++++ app/public/guild/economyForm.js | 68 ++++++++ app/routes/api.js | 117 ++++++++++++++ 20 files changed, 2123 insertions(+), 9 deletions(-) create mode 100644 app/commands/adminAddXp.js create mode 100644 app/commands/adminEcoAdd.js create mode 100644 app/commands/adminEcoRemove.js create mode 100644 app/commands/adminRemoveXp.js create mode 100644 app/commands/adminSetXp.js create mode 100644 app/commands/balance.js create mode 100644 app/commands/crime.js create mode 100644 app/commands/daily.js create mode 100644 app/commands/deposit.js create mode 100644 app/commands/pay.js create mode 100644 app/commands/richest.js create mode 100644 app/commands/withdraw.js create mode 100644 app/commands/work.js create mode 100644 app/public/guild/economyForm.js diff --git a/app/commands/adminAddXp.js b/app/commands/adminAddXp.js new file mode 100644 index 0000000..cee55c5 --- /dev/null +++ b/app/commands/adminAddXp.js @@ -0,0 +1,224 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, PermissionFlagsBits } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "adminaddxp", + description: "Ajoute de l'XP ou des niveaux à un utilisateur (admin uniquement).", + aliases: ["addxp", "givexp", "addlevel"], + permissions: [PermissionFlagsBits.Administrator], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM levels_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur à qui ajouter l'XP ou les niveaux.", + required: true, + }, + { + type: "STRING", + name: "type", + description: "Type d'ajout : 'xp' ou 'level'.", + required: true, + choices: [ + { name: "XP", value: "xp" }, + { name: "Niveau", value: "level" }, + ], + }, + { + type: "INTEGER", + name: "quantite", + description: "Quantité d'XP ou de niveaux à ajouter.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + + // Vérifier les arguments : !adminaddxp @user xp/level quantité + if (args.length < 3) { + return message.reply("Usage : `!adminaddxp @utilisateur `"); + } + + const userMention = message.mentions.users.first(); + if (!userMention) { + return message.reply("Veuillez mentionner un utilisateur valide."); + } + + const type = args[1].toLowerCase(); + if (type !== "xp" && type !== "level") { + return message.reply("Le type doit être `xp` ou `level`."); + } + + const quantite = parseInt(args[2], 10); + if (isNaN(quantite) || quantite <= 0) { + return message.reply("La quantité doit être un nombre positif."); + } + + await processAddXp(guildId, userMention.id, type, quantite, message.guild, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + + const user = interaction.options.getUser("utilisateur"); + const type = interaction.options.getString("type").toLowerCase(); + const quantite = interaction.options.getInteger("quantite"); + + if (type !== "xp" && type !== "level") { + return interaction.reply({ content: "Le type doit être `xp` ou `level`.", ephemeral: true }); + } + + if (quantite <= 0) { + return interaction.reply({ content: "La quantité doit être un nombre positif.", ephemeral: true }); + } + + await processAddXp(guildId, user.id, type, quantite, interaction.guild, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processAddXp(guildId, userId, type, quantite, guild, onSuccess, onError) { + // Récupérer la config des niveaux + db.get( + `SELECT + xp_courbe_type, + multiplier_courbe_for_level, + level_max, + level_announcements_enabled, + level_announcements_channel_id, + level_announcements_message, + level_annoncement_every_level + FROM levels_config WHERE guild_id = ?`, + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système de niveaux n'est pas configuré sur ce serveur."); + } + + // Récupérer les données actuelles de l'utilisateur + db.get( + `SELECT xp, level FROM user_levels WHERE guild_id = ? AND user_id = ?`, + [guildId, userId], + (err, userRow) => { + if (err) { + return onError("Erreur lors de la récupération des données de l'utilisateur."); + } + + let currentXp = userRow ? userRow.xp : 0; + let currentLevel = userRow ? userRow.level : 1; + + // Fonction de courbe + const multiplier = config.multiplier_courbe_for_level; + let fonction_courbe; + + if (config.xp_courbe_type === "constante") { + fonction_courbe = (level) => multiplier; + } else if (config.xp_courbe_type === "linear") { + fonction_courbe = (level) => level * multiplier; + } else if (config.xp_courbe_type === "quadratic") { + fonction_courbe = (level) => level * level * multiplier; + } else if (config.xp_courbe_type === "exponential") { + fonction_courbe = (level) => Math.pow(2, level - 1) * multiplier; + } + + let newXp = currentXp; + let newLevel = currentLevel; + + if (type === "xp") { + newXp += quantite; + } else if (type === "level") { + // Ajouter les niveaux directement + for (let i = 0; i < quantite; i++) { + if (config.level_max === 0 || newLevel < config.level_max) { + newLevel += 1; + } + } + newXp = 0; // Reset XP au début du nouveau niveau + } + + // Recalculer les niveaux si on a ajouté de l'XP + if (type === "xp") { + let xpForNextLevel = fonction_courbe(newLevel); + while (newXp >= xpForNextLevel && (config.level_max === 0 || newLevel < config.level_max)) { + newXp -= xpForNextLevel; + newLevel += 1; + xpForNextLevel = fonction_courbe(newLevel); + + // Annonce si activée + if (config.level_announcements_enabled && (newLevel % config.level_annoncement_every_level === 0)) { + const channel = guild.channels.cache.get(config.level_announcements_channel_id); + if (channel) { + const member = guild.members.cache.get(userId); + let announcementMsg = config.level_announcements_message; + announcementMsg = announcementMsg + .replace("{user}", member?.user?.username || "Utilisateur") + .replace("{mention}", `<@${userId}>`) + .replace("{level}", newLevel) + .replace("{level-xp}", fonction_courbe(newLevel)); + channel.send(announcementMsg); + } + } + } + } + + // Sauvegarder en base + db.run( + `INSERT INTO user_levels (guild_id, user_id, xp, level) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + xp = excluded.xp, + level = excluded.level`, + [guildId, userId, newXp, newLevel], + (err) => { + if (err) { + return onError("Erreur lors de la sauvegarde des données."); + } + + const embed = new EmbedBuilder() + .setTitle("✅ XP/Niveau ajouté") + .setColor("#00FF00") + .setDescription( + type === "xp" + ? `**${quantite} XP** ajouté à <@${userId}>.\n\n` + + `**Niveau actuel :** ${newLevel}\n**XP actuel :** ${newXp}` + : `**${quantite} niveau(x)** ajouté(s) à <@${userId}>.\n\n` + + `**Niveau actuel :** ${newLevel}\n**XP actuel :** ${newXp}` + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} \ No newline at end of file diff --git a/app/commands/adminEcoAdd.js b/app/commands/adminEcoAdd.js new file mode 100644 index 0000000..edf9f73 --- /dev/null +++ b/app/commands/adminEcoAdd.js @@ -0,0 +1,108 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, PermissionFlagsBits } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "adminecoadd", + description: "Ajoute de l'argent à un utilisateur (admin uniquement).", + aliases: ["addmoney", "givemoney"], + permissions: [PermissionFlagsBits.Administrator], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur à qui ajouter l'argent.", + required: true, + }, + { + type: "INTEGER", + name: "montant", + description: "Le montant à ajouter.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const targetUser = message.mentions.users.first(); + if (!targetUser) return message.reply("Veuillez mentionner un utilisateur."); + + const amount = parseInt(args[1], 10); + if (isNaN(amount) || amount <= 0) return message.reply("Veuillez entrer un montant valide."); + + await processAdminAdd(message.guild.id, targetUser, amount, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const targetUser = interaction.options.getUser("utilisateur"); + const amount = interaction.options.getInteger("montant"); + + if (amount <= 0) { + return interaction.reply({ content: "Le montant doit être positif.", ephemeral: true }); + } + + await processAdminAdd(interaction.guild.id, targetUser, amount, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processAdminAdd(guildId, user, amount, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.get( + "SELECT balance FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const newBalance = (row?.balance || 0) + amount; + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank) + VALUES (?, ?, ?, 0) + ON CONFLICT(guild_id, user_id) DO UPDATE SET balance = ?`, + [guildId, user.id, newBalance, newBalance], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Argent ajouté`) + .setColor("#00FF00") + .setDescription(`**${amount.toLocaleString()} ${config.currency_name}** ajouté à ${user}.\n\nNouveau solde : **${newBalance.toLocaleString()} ${config.currency_name}**`) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/adminEcoRemove.js b/app/commands/adminEcoRemove.js new file mode 100644 index 0000000..7bd1f96 --- /dev/null +++ b/app/commands/adminEcoRemove.js @@ -0,0 +1,108 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, PermissionFlagsBits } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "adminecoremove", + description: "Retire de l'argent à un utilisateur (admin uniquement).", + aliases: ["removemoney", "takemoney"], + permissions: [PermissionFlagsBits.Administrator], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur à qui retirer l'argent.", + required: true, + }, + { + type: "INTEGER", + name: "montant", + description: "Le montant à retirer.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const targetUser = message.mentions.users.first(); + if (!targetUser) return message.reply("Veuillez mentionner un utilisateur."); + + const amount = parseInt(args[1], 10); + if (isNaN(amount) || amount <= 0) return message.reply("Veuillez entrer un montant valide."); + + await processAdminRemove(message.guild.id, targetUser, amount, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const targetUser = interaction.options.getUser("utilisateur"); + const amount = interaction.options.getInteger("montant"); + + if (amount <= 0) { + return interaction.reply({ content: "Le montant doit être positif.", ephemeral: true }); + } + + await processAdminRemove(interaction.guild.id, targetUser, amount, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processAdminRemove(guildId, user, amount, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.get( + "SELECT balance FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const newBalance = Math.max(0, (row?.balance || 0) - amount); + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank) + VALUES (?, ?, ?, 0) + ON CONFLICT(guild_id, user_id) DO UPDATE SET balance = ?`, + [guildId, user.id, newBalance, newBalance], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Argent retiré`) + .setColor("#FF0000") + .setDescription(`**${amount.toLocaleString()} ${config.currency_name}** retiré à ${user}.\n\nNouveau solde : **${newBalance.toLocaleString()} ${config.currency_name}**`) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/adminRemoveXp.js b/app/commands/adminRemoveXp.js new file mode 100644 index 0000000..4da44fd --- /dev/null +++ b/app/commands/adminRemoveXp.js @@ -0,0 +1,198 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, PermissionFlagsBits } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "adminremovexp", + description: "Retire de l'XP ou des niveaux à un utilisateur (admin uniquement).", + aliases: ["removexp", "takexp", "removelevel"], + permissions: [PermissionFlagsBits.Administrator], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM levels_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur à qui retirer l'XP ou les niveaux.", + required: true, + }, + { + type: "STRING", + name: "type", + description: "Type de retrait : 'xp' ou 'level'.", + required: true, + choices: [ + { name: "XP", value: "xp" }, + { name: "Niveau", value: "level" }, + ], + }, + { + type: "INTEGER", + name: "quantite", + description: "Quantité d'XP ou de niveaux à retirer.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + + // Vérifier les arguments : !adminremovexp @user xp/level quantité + if (args.length < 3) { + return message.reply("Usage : `!adminremovexp @utilisateur `"); + } + + const userMention = message.mentions.users.first(); + if (!userMention) { + return message.reply("Veuillez mentionner un utilisateur valide."); + } + + const type = args[1].toLowerCase(); + if (type !== "xp" && type !== "level") { + return message.reply("Le type doit être `xp` ou `level`."); + } + + const quantite = parseInt(args[2], 10); + if (isNaN(quantite) || quantite <= 0) { + return message.reply("La quantité doit être un nombre positif."); + } + + await processRemoveXp(guildId, userMention.id, type, quantite, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + + const user = interaction.options.getUser("utilisateur"); + const type = interaction.options.getString("type"); + const quantite = interaction.options.getInteger("quantite"); + + if (quantite <= 0) { + return interaction.reply({ content: "La quantité doit être un nombre positif.", ephemeral: true }); + } + + await processRemoveXp(guildId, user.id, type, quantite, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processRemoveXp(guildId, userId, type, quantite, onSuccess, onError) { + // Récupérer la config des niveaux + db.get( + `SELECT + xp_courbe_type, + multiplier_courbe_for_level + FROM levels_config WHERE guild_id = ?`, + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système de niveaux n'est pas configuré sur ce serveur."); + } + + // Récupérer les données actuelles de l'utilisateur + db.get( + `SELECT xp, level FROM user_levels WHERE guild_id = ? AND user_id = ?`, + [guildId, userId], + (err, userRow) => { + if (err) { + return onError("Erreur lors de la récupération des données de l'utilisateur."); + } + + if (!userRow) { + return onError("Cet utilisateur n'a pas encore de données de niveau."); + } + + let currentXp = userRow.xp; + let currentLevel = userRow.level; + + // Fonction de courbe + const multiplier = config.multiplier_courbe_for_level; + let fonction_courbe; + + if (config.xp_courbe_type === "constante") { + fonction_courbe = (level) => multiplier; + } else if (config.xp_courbe_type === "linear") { + fonction_courbe = (level) => level * multiplier; + } else if (config.xp_courbe_type === "quadratic") { + fonction_courbe = (level) => level * level * multiplier; + } else if (config.xp_courbe_type === "exponential") { + fonction_courbe = (level) => Math.pow(2, level - 1) * multiplier; + } + + let newXp = currentXp; + let newLevel = currentLevel; + + if (type === "xp") { + newXp -= quantite; + // Gérer les niveaux en négatif + while (newXp < 0 && newLevel > 1) { + newLevel -= 1; + newXp += fonction_courbe(newLevel); + } + // S'assurer que l'XP ne soit pas négatif + if (newXp < 0) { + newXp = 0; + } + } else if (type === "level") { + // Retirer les niveaux directement + newLevel -= quantite; + if (newLevel < 1) { + newLevel = 1; + } + newXp = 0; // Reset XP + } + + // Sauvegarder en base + db.run( + `UPDATE user_levels SET xp = ?, level = ? WHERE guild_id = ? AND user_id = ?`, + [newXp, newLevel, guildId, userId], + (err) => { + if (err) { + return onError("Erreur lors de la sauvegarde des données."); + } + + const embed = new EmbedBuilder() + .setTitle("❌ XP/Niveau retiré") + .setColor("#FF0000") + .setDescription( + type === "xp" + ? `**${quantite} XP** retiré à <@${userId}>.\n\n` + + `**Niveau actuel :** ${newLevel}\n**XP actuel :** ${newXp}` + : `**${quantite} niveau(x)** retiré(s) à <@${userId}>.\n\n` + + `**Niveau actuel :** ${newLevel}\n**XP actuel :** ${newXp}` + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/adminSetXp.js b/app/commands/adminSetXp.js new file mode 100644 index 0000000..8f6cf31 --- /dev/null +++ b/app/commands/adminSetXp.js @@ -0,0 +1,187 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder, PermissionFlagsBits } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "adminsetxp", + description: "Définit l'XP ou le niveau d'un utilisateur (admin uniquement).", + aliases: ["setxp", "setlevel"], + permissions: [PermissionFlagsBits.Administrator], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM levels_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur dont définir l'XP ou le niveau.", + required: true, + }, + { + type: "STRING", + name: "type", + description: "Ce que vous voulez définir : 'xp' ou 'level'.", + required: true, + choices: [ + { name: "XP", value: "xp" }, + { name: "Niveau", value: "level" }, + ], + }, + { + type: "INTEGER", + name: "valeur", + description: "La nouvelle valeur d'XP ou de niveau.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + + // Vérifier les arguments : !adminsetxp @user xp/level valeur + if (args.length < 3) { + return message.reply("Usage : `!adminsetxp @utilisateur `"); + } + + const userMention = message.mentions.users.first(); + if (!userMention) { + return message.reply("Veuillez mentionner un utilisateur valide."); + } + + const type = args[1].toLowerCase(); + if (type !== "xp" && type !== "level") { + return message.reply("Le type doit être `xp` ou `level`."); + } + + const valeur = parseInt(args[2], 10); + if (isNaN(valeur) || valeur < 0) { + return message.reply("La valeur doit être un nombre positif ou zéro."); + } + + await processSetXp(guildId, userMention.id, type, valeur, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + + const user = interaction.options.getUser("utilisateur"); + const type = interaction.options.getString("type"); + const valeur = interaction.options.getInteger("valeur"); + + if (valeur < 0) { + return interaction.reply({ content: "La valeur doit être un nombre positif ou zéro.", ephemeral: true }); + } + + await processSetXp(guildId, user.id, type, valeur, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processSetXp(guildId, userId, type, valeur, onSuccess, onError) { + // Récupérer la config des niveaux + db.get( + `SELECT + xp_courbe_type, + multiplier_courbe_for_level, + level_max + FROM levels_config WHERE guild_id = ?`, + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système de niveaux n'est pas configuré sur ce serveur."); + } + + // Fonction de courbe + const multiplier = config.multiplier_courbe_for_level; + let fonction_courbe; + + if (config.xp_courbe_type === "constante") { + fonction_courbe = (level) => multiplier; + } else if (config.xp_courbe_type === "linear") { + fonction_courbe = (level) => level * multiplier; + } else if (config.xp_courbe_type === "quadratic") { + fonction_courbe = (level) => level * level * multiplier; + } else if (config.xp_courbe_type === "exponential") { + fonction_courbe = (level) => Math.pow(2, level - 1) * multiplier; + } + + let newXp, newLevel; + + if (type === "xp") { + // Définir l'XP et recalculer le niveau + newXp = valeur; + newLevel = 1; + + let xpForNextLevel = fonction_courbe(newLevel); + while (newXp >= xpForNextLevel && (config.level_max === 0 || newLevel < config.level_max)) { + newXp -= xpForNextLevel; + newLevel += 1; + xpForNextLevel = fonction_courbe(newLevel); + } + } else if (type === "level") { + // Définir le niveau directement + newLevel = valeur; + if (newLevel < 1) { + newLevel = 1; + } + if (config.level_max > 0 && newLevel > config.level_max) { + newLevel = config.level_max; + } + newXp = 0; // Reset XP au début du niveau + } + + // Sauvegarder en base (INSERT ou UPDATE) + db.run( + `INSERT INTO user_levels (guild_id, user_id, xp, level) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + xp = excluded.xp, + level = excluded.level`, + [guildId, userId, newXp, newLevel], + (err) => { + if (err) { + return onError("Erreur lors de la sauvegarde des données."); + } + + const embed = new EmbedBuilder() + .setTitle("⚙️ XP/Niveau défini") + .setColor("#0099FF") + .setDescription( + type === "xp" + ? `L'XP de <@${userId}> a été défini.\n\n` + + `**Niveau actuel :** ${newLevel}\n**XP actuel :** ${newXp}` + : `Le niveau de <@${userId}> a été défini à **${newLevel}**.\n\n` + + `**XP actuel :** ${newXp}` + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); +} diff --git a/app/commands/balance.js b/app/commands/balance.js new file mode 100644 index 0000000..0f45171 --- /dev/null +++ b/app/commands/balance.js @@ -0,0 +1,99 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "balance", + description: "Affiche votre solde ou celui d'un autre utilisateur.", + aliases: ["bal", "money", "coins", "solde"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) { + console.error(`DB error in guildCondition for guild ${guildId}`, err); + return resolve(false); + } + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur dont afficher le solde.", + required: false, + }, + ], + + executePrefix: async (client, message, args) => { + const guildId = message.guild.id; + const targetUser = message.mentions.users.first() || message.author; + + await showBalance(guildId, targetUser, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const guildId = interaction.guild.id; + const targetUser = interaction.options.getUser("utilisateur") || interaction.user; + + await showBalance(guildId, targetUser, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function showBalance(guildId, user, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système d'économie n'est pas configuré sur ce serveur."); + } + + db.get( + "SELECT balance, bank FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) { + return onError("Erreur lors de la récupération du solde."); + } + + const balance = row?.balance || 0; + const bank = row?.bank || 0; + const total = balance + bank; + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Solde de ${user.username}`) + .setColor("#FFD700") + .setThumbnail(user.displayAvatarURL()) + .addFields( + { name: "💵 Portefeuille", value: `${balance.toLocaleString()} ${config.currency_name}`, inline: true }, + { name: "🏦 Banque", value: `${bank.toLocaleString()} ${config.currency_name}`, inline: true }, + { name: "💰 Total", value: `${total.toLocaleString()} ${config.currency_name}`, inline: false } + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); +} diff --git a/app/commands/crime.js b/app/commands/crime.js new file mode 100644 index 0000000..597b0a9 --- /dev/null +++ b/app/commands/crime.js @@ -0,0 +1,138 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +const crimeSuccessMessages = [ + "Vous avez cambriolé une maison et volé", + "Vous avez piraté un distributeur et récupéré", + "Vous avez arnaqué quelqu'un et obtenu", + "Vous avez trouvé un portefeuille abandonné contenant", + "Vous avez vendu des objets volés pour", + "Vous avez braqué une épicerie et pris", + "Vous avez piraté un compte bancaire et transféré" +]; + +const crimeFailMessages = [ + "Vous vous êtes fait attraper par la police !", + "Un témoin vous a dénoncé !", + "Les caméras de surveillance vous ont repéré !", + "Vous avez glissé sur une peau de banane en fuyant !", + "Un chien de garde vous a mordu !", + "Vous avez déclenché l'alarme !", + "La victime connaissait du karaté !" +]; + +module.exports = addCommand({ + name: "crime", + description: "Tentez un crime risqué pour de l'argent.", + aliases: ["vol", "steal", "rob"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [], + + executePrefix: async (client, message, args) => { + await doCrime(message.guild.id, message.author, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + await doCrime(interaction.guild.id, interaction.user, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function doCrime(guildId, user, onSuccess, onError) { + db.get( + `SELECT currency_name, currency_symbol, crime_min_amount, crime_max_amount, + crime_success_rate, crime_fine_percent, crime_cooldown_minutes + FROM economy_config WHERE guild_id = ?`, + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système d'économie n'est pas configuré."); + } + + db.get( + "SELECT balance, last_crime_timestamp FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const now = Date.now(); + const cooldownMs = config.crime_cooldown_minutes * 60 * 1000; + const lastCrime = row?.last_crime_timestamp || 0; + + if (now - lastCrime < cooldownMs) { + const timeLeft = cooldownMs - (now - lastCrime); + const minutes = Math.floor(timeLeft / (60 * 1000)); + const seconds = Math.floor((timeLeft % (60 * 1000)) / 1000); + return onError(`⏰ Vous devez attendre encore **${minutes}m ${seconds}s** avant de pouvoir commettre un autre crime.`); + } + + const currentBalance = row?.balance || 0; + const success = Math.random() * 100 < config.crime_success_rate; + + let newBalance; + let embed; + + if (success) { + const earned = Math.floor(Math.random() * (config.crime_max_amount - config.crime_min_amount + 1)) + config.crime_min_amount; + newBalance = currentBalance + earned; + const crimeMsg = crimeSuccessMessages[Math.floor(Math.random() * crimeSuccessMessages.length)]; + + embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Crime réussi !`) + .setColor("#00FF00") + .setDescription(`${crimeMsg} **${earned.toLocaleString()} ${config.currency_name}** !`) + .setTimestamp(); + } else { + const fine = Math.floor(currentBalance * (config.crime_fine_percent / 100)); + newBalance = Math.max(0, currentBalance - fine); + const failMsg = crimeFailMessages[Math.floor(Math.random() * crimeFailMessages.length)]; + + embed = new EmbedBuilder() + .setTitle("🚔 Crime échoué !") + .setColor("#FF0000") + .setDescription(`${failMsg}\n\nVous avez payé une amende de **${fine.toLocaleString()} ${config.currency_name}**.`) + .setTimestamp(); + } + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank, last_crime_timestamp) + VALUES (?, ?, ?, 0, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + balance = ?, + last_crime_timestamp = ?`, + [guildId, user.id, newBalance, now, newBalance, now], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/daily.js b/app/commands/daily.js new file mode 100644 index 0000000..64cad7b --- /dev/null +++ b/app/commands/daily.js @@ -0,0 +1,97 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "daily", + description: "Récupérez votre récompense quotidienne.", + aliases: ["quotidien", "journalier"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [], + + executePrefix: async (client, message, args) => { + await claimDaily(message.guild.id, message.author, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + await claimDaily(interaction.guild.id, interaction.user, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function claimDaily(guildId, user, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol, daily_amount, daily_cooldown_hours FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système d'économie n'est pas configuré."); + } + + db.get( + "SELECT balance, last_daily_timestamp FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const now = Date.now(); + const cooldownMs = config.daily_cooldown_hours * 60 * 60 * 1000; + const lastDaily = row?.last_daily_timestamp || 0; + + if (now - lastDaily < cooldownMs) { + const timeLeft = cooldownMs - (now - lastDaily); + const hours = Math.floor(timeLeft / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + return onError(`⏰ Vous devez attendre encore **${hours}h ${minutes}m** avant de récupérer votre récompense quotidienne.`); + } + + const newBalance = (row?.balance || 0) + config.daily_amount; + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank, last_daily_timestamp) + VALUES (?, ?, ?, 0, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + balance = ?, + last_daily_timestamp = ?`, + [guildId, user.id, newBalance, now, newBalance, now], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Récompense quotidienne`) + .setColor("#00FF00") + .setDescription(`Vous avez récupéré **${config.daily_amount.toLocaleString()} ${config.currency_name}** !\n\nNouveau solde : **${newBalance.toLocaleString()} ${config.currency_name}**`) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/deposit.js b/app/commands/deposit.js new file mode 100644 index 0000000..695cb5d --- /dev/null +++ b/app/commands/deposit.js @@ -0,0 +1,118 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "deposit", + description: "Déposez de l'argent à la banque.", + aliases: ["dep", "deposer"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "STRING", + name: "montant", + description: "Le montant à déposer (nombre ou 'all').", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + if (!args[0]) return message.reply("Usage : `!deposit `"); + + await processDeposit(message.guild.id, message.author.id, args[0], (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const montant = interaction.options.getString("montant"); + + await processDeposit(interaction.guild.id, interaction.user.id, montant, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processDeposit(guildId, userId, amountStr, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.get( + "SELECT balance, bank FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, userId], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const currentBalance = row?.balance || 0; + const currentBank = row?.bank || 0; + + let amount; + if (amountStr.toLowerCase() === "all") { + amount = currentBalance; + } else { + amount = parseInt(amountStr, 10); + } + + if (isNaN(amount) || amount <= 0) { + return onError("Veuillez entrer un montant valide."); + } + + if (amount > currentBalance) { + return onError(`Vous n'avez pas assez d'argent. Solde actuel : **${currentBalance.toLocaleString()} ${config.currency_name}**`); + } + + const newBalance = currentBalance - amount; + const newBank = currentBank + amount; + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank) + VALUES (?, ?, ?, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + balance = ?, + bank = ?`, + [guildId, userId, newBalance, newBank, newBalance, newBank], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`🏦 Dépôt effectué`) + .setColor("#00FF00") + .setDescription(`Vous avez déposé **${amount.toLocaleString()} ${config.currency_name}** à la banque.`) + .addFields( + { name: "💵 Portefeuille", value: `${newBalance.toLocaleString()} ${config.currency_name}`, inline: true }, + { name: "🏦 Banque", value: `${newBank.toLocaleString()} ${config.currency_name}`, inline: true } + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/pay.js b/app/commands/pay.js new file mode 100644 index 0000000..2cfbb1a --- /dev/null +++ b/app/commands/pay.js @@ -0,0 +1,147 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "pay", + description: "Envoyez de l'argent à un autre utilisateur.", + aliases: ["give", "transfer", "envoyer"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "USER", + name: "utilisateur", + description: "L'utilisateur à qui envoyer l'argent.", + required: true, + }, + { + type: "INTEGER", + name: "montant", + description: "Le montant à envoyer.", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + const targetUser = message.mentions.users.first(); + if (!targetUser) return message.reply("Veuillez mentionner un utilisateur."); + + const amount = parseInt(args[1], 10); + if (isNaN(amount) || amount <= 0) return message.reply("Veuillez entrer un montant valide."); + + await processPay(message.guild.id, message.author, targetUser, amount, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const targetUser = interaction.options.getUser("utilisateur"); + const amount = interaction.options.getInteger("montant"); + + if (amount <= 0) { + return interaction.reply({ content: "Le montant doit être positif.", ephemeral: true }); + } + + await processPay(interaction.guild.id, interaction.user, targetUser, amount, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processPay(guildId, sender, receiver, amount, onSuccess, onError) { + if (sender.id === receiver.id) { + return onError("Vous ne pouvez pas vous envoyer de l'argent à vous-même."); + } + + if (receiver.bot) { + return onError("Vous ne pouvez pas envoyer de l'argent à un bot."); + } + + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.get( + "SELECT balance FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, sender.id], + (err, senderRow) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const senderBalance = senderRow?.balance || 0; + + if (amount > senderBalance) { + return onError(`Vous n'avez pas assez d'argent. Solde : **${senderBalance.toLocaleString()} ${config.currency_name}**`); + } + + db.get( + "SELECT balance FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, receiver.id], + (err, receiverRow) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const receiverBalance = receiverRow?.balance || 0; + const newSenderBalance = senderBalance - amount; + const newReceiverBalance = receiverBalance + amount; + + // Update sender + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank) + VALUES (?, ?, ?, 0) + ON CONFLICT(guild_id, user_id) DO UPDATE SET balance = ?`, + [guildId, sender.id, newSenderBalance, newSenderBalance], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + // Update receiver + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank) + VALUES (?, ?, ?, 0) + ON CONFLICT(guild_id, user_id) DO UPDATE SET balance = ?`, + [guildId, receiver.id, newReceiverBalance, newReceiverBalance], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Transfert effectué`) + .setColor("#00FF00") + .setDescription(`Vous avez envoyé **${amount.toLocaleString()} ${config.currency_name}** à ${receiver}.`) + .addFields( + { name: "Votre nouveau solde", value: `${newSenderBalance.toLocaleString()} ${config.currency_name}`, inline: true } + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); + } + ); + } + ); +} diff --git a/app/commands/richest.js b/app/commands/richest.js new file mode 100644 index 0000000..23085a3 --- /dev/null +++ b/app/commands/richest.js @@ -0,0 +1,82 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "richest", + description: "Affiche le classement des plus riches du serveur.", + aliases: ["baltop", "leaderboard", "lb", "moneytop"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [], + + executePrefix: async (client, message, args) => { + await showLeaderboard(message.guild.id, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + await showLeaderboard(interaction.guild.id, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function showLeaderboard(guildId, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.all( + `SELECT user_id, (balance + bank) as total FROM user_economy + WHERE guild_id = ? ORDER BY total DESC LIMIT 10`, + [guildId], + (err, rows) => { + if (err) return onError("Erreur lors de la récupération du classement."); + + if (!rows || rows.length === 0) { + return onError("Aucun utilisateur n'a encore d'argent sur ce serveur."); + } + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Top 10 des plus riches`) + .setColor("#FFD700") + .setDescription( + rows + .map((row, index) => { + const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `**${index + 1}.**`; + return `${medal} <@${row.user_id}> - **${row.total.toLocaleString()}** ${config.currency_name}`; + }) + .join("\n") + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); +} diff --git a/app/commands/withdraw.js b/app/commands/withdraw.js new file mode 100644 index 0000000..3d3ef30 --- /dev/null +++ b/app/commands/withdraw.js @@ -0,0 +1,114 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +module.exports = addCommand({ + name: "withdraw", + description: "Retirez de l'argent de la banque.", + aliases: ["with", "retirer"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [ + { + type: "STRING", + name: "montant", + description: "Le montant à retirer (nombre ou 'all').", + required: true, + }, + ], + + executePrefix: async (client, message, args) => { + if (!args[0]) return message.reply("Usage : `!withdraw `"); + + await processWithdraw(message.guild.id, message.author.id, args[0], (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + const montant = interaction.options.getString("montant"); + + await processWithdraw(interaction.guild.id, interaction.user.id, montant, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function processWithdraw(guildId, userId, amountStr, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) return onError("Le système d'économie n'est pas configuré."); + + db.get( + "SELECT balance, bank FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, userId], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const currentBalance = row?.balance || 0; + const currentBank = row?.bank || 0; + + let amount; + if (amountStr.toLowerCase() === "all") { + amount = currentBank; + } else { + amount = parseInt(amountStr, 10); + } + + if (isNaN(amount) || amount <= 0) { + return onError("Veuillez entrer un montant valide."); + } + + if (amount > currentBank) { + return onError(`Vous n'avez pas assez d'argent en banque. Solde banque : **${currentBank.toLocaleString()} ${config.currency_name}**`); + } + + const newBalance = currentBalance + amount; + const newBank = currentBank - amount; + + db.run( + `UPDATE user_economy SET balance = ?, bank = ? WHERE guild_id = ? AND user_id = ?`, + [newBalance, newBank, guildId, userId], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`🏦 Retrait effectué`) + .setColor("#00FF00") + .setDescription(`Vous avez retiré **${amount.toLocaleString()} ${config.currency_name}** de la banque.`) + .addFields( + { name: "💵 Portefeuille", value: `${newBalance.toLocaleString()} ${config.currency_name}`, inline: true }, + { name: "🏦 Banque", value: `${newBank.toLocaleString()} ${config.currency_name}`, inline: true } + ) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/commands/work.js b/app/commands/work.js new file mode 100644 index 0000000..8253796 --- /dev/null +++ b/app/commands/work.js @@ -0,0 +1,114 @@ +const addCommand = require("../fonctions/addCommand"); +const { EmbedBuilder } = require("discord.js"); +const db = require("../db"); + +const workMessages = [ + "Vous avez travaillé comme serveur et gagné", + "Vous avez tondu des pelouses et gagné", + "Vous avez livré des pizzas et gagné", + "Vous avez aidé un voisin à déménager et gagné", + "Vous avez fait du baby-sitting et gagné", + "Vous avez lavé des voitures et gagné", + "Vous avez promené des chiens et gagné", + "Vous avez vendu des limonades et gagné", + "Vous avez travaillé comme caissier et gagné", + "Vous avez donné des cours particuliers et gagné", + "Vous avez réparé des ordinateurs et gagné", + "Vous avez fait du jardinage et gagné" +]; + +module.exports = addCommand({ + name: "work", + description: "Travaillez pour gagner de l'argent.", + aliases: ["travail", "travailler", "job"], + permissions: [], + botOwnerOnly: false, + dm: false, + scope: "guild", + + guildCondition: async (guildId) => { + return new Promise((resolve) => { + db.get( + "SELECT enabled FROM economy_config WHERE guild_id = ?", + [guildId], + (err, row) => { + if (err) return resolve(false); + resolve(!!row?.enabled); + } + ); + }); + }, + + slashOptions: [], + + executePrefix: async (client, message, args) => { + await doWork(message.guild.id, message.author, (embed) => { + message.reply({ embeds: [embed] }); + }, (errMsg) => { + message.reply(errMsg); + }); + }, + + executeSlash: async (client, interaction) => { + await doWork(interaction.guild.id, interaction.user, (embed) => { + interaction.reply({ embeds: [embed] }); + }, (errMsg) => { + interaction.reply({ content: errMsg, ephemeral: true }); + }); + }, +}); + +async function doWork(guildId, user, onSuccess, onError) { + db.get( + "SELECT currency_name, currency_symbol, work_min_amount, work_max_amount, work_cooldown_minutes FROM economy_config WHERE guild_id = ?", + [guildId], + (err, config) => { + if (err || !config) { + return onError("Le système d'économie n'est pas configuré."); + } + + db.get( + "SELECT balance, last_work_timestamp FROM user_economy WHERE guild_id = ? AND user_id = ?", + [guildId, user.id], + (err, row) => { + if (err) return onError("Erreur lors de la récupération des données."); + + const now = Date.now(); + const cooldownMs = config.work_cooldown_minutes * 60 * 1000; + const lastWork = row?.last_work_timestamp || 0; + + if (now - lastWork < cooldownMs) { + const timeLeft = cooldownMs - (now - lastWork); + const minutes = Math.floor(timeLeft / (60 * 1000)); + const seconds = Math.floor((timeLeft % (60 * 1000)) / 1000); + return onError(`⏰ Vous devez attendre encore **${minutes}m ${seconds}s** avant de pouvoir retravailler.`); + } + + const earned = Math.floor(Math.random() * (config.work_max_amount - config.work_min_amount + 1)) + config.work_min_amount; + const newBalance = (row?.balance || 0) + earned; + const workMsg = workMessages[Math.floor(Math.random() * workMessages.length)]; + + db.run( + `INSERT INTO user_economy (guild_id, user_id, balance, bank, last_work_timestamp) + VALUES (?, ?, ?, 0, ?) + ON CONFLICT(guild_id, user_id) DO UPDATE SET + balance = ?, + last_work_timestamp = ?`, + [guildId, user.id, newBalance, now, newBalance, now], + (err) => { + if (err) return onError("Erreur lors de la sauvegarde."); + + const embed = new EmbedBuilder() + .setTitle(`${config.currency_symbol} Travail`) + .setColor("#00FF00") + .setDescription(`${workMsg} **${earned.toLocaleString()} ${config.currency_name}** !`) + .setTimestamp(); + + onSuccess(embed); + } + ); + } + ); + } + ); +} diff --git a/app/db.js b/app/db.js index 44dedc0..d72b739 100644 --- a/app/db.js +++ b/app/db.js @@ -93,6 +93,56 @@ db.exec(` prefix TEXT NOT NULL DEFAULT '!', PRIMARY KEY (guildId) ); + + CREATE TABLE IF NOT EXISTS economy_config ( + guild_id TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 0, + currency_name TEXT NOT NULL DEFAULT 'coins', + currency_symbol TEXT NOT NULL DEFAULT '💰', + daily_amount INTEGER NOT NULL DEFAULT 100, + daily_cooldown_hours INTEGER NOT NULL DEFAULT 24, + work_min_amount INTEGER NOT NULL DEFAULT 50, + work_max_amount INTEGER NOT NULL DEFAULT 150, + work_cooldown_minutes INTEGER NOT NULL DEFAULT 60, + crime_min_amount INTEGER NOT NULL DEFAULT 100, + crime_max_amount INTEGER NOT NULL DEFAULT 500, + crime_success_rate INTEGER NOT NULL DEFAULT 50, + crime_fine_percent INTEGER NOT NULL DEFAULT 30, + crime_cooldown_minutes INTEGER NOT NULL DEFAULT 120, + starting_balance INTEGER NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS user_economy ( + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + balance INTEGER NOT NULL DEFAULT 0, + bank INTEGER NOT NULL DEFAULT 0, + last_daily_timestamp INTEGER, + last_work_timestamp INTEGER, + last_crime_timestamp INTEGER, + PRIMARY KEY (guild_id, user_id) + ); + + CREATE TABLE IF NOT EXISTS shop_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + price INTEGER NOT NULL, + role_id TEXT, + stock INTEGER DEFAULT -1, + created_at INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS user_inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + user_id TEXT NOT NULL, + item_id INTEGER NOT NULL, + quantity INTEGER NOT NULL DEFAULT 1, + purchased_at INTEGER NOT NULL, + FOREIGN KEY (item_id) REFERENCES shop_items(id) + ); `); module.exports = db; diff --git a/app/events/executeCommands/prefix.js b/app/events/executeCommands/prefix.js index 017c018..189216d 100644 --- a/app/events/executeCommands/prefix.js +++ b/app/events/executeCommands/prefix.js @@ -24,6 +24,28 @@ module.exports = { ); if (!command) return; + // Vérification du scope / guildCondition + if (command.scope === "guild") { + const guildId = message.guild ? message.guild.id : null; + if (!guildId) + return message + .reply({ content: "Cette commande ne peut pas être utilisée en message privé." }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); + if (typeof command.guildCondition === "function") { + let allowed = false; + try { + allowed = await command.guildCondition(guildId); + } catch (err) { + console.error(`Erreur guildCondition pour la guild ${guildId}`, err); + allowed = false; + } + if (!allowed) + return message + .reply({ content: "Cette commande est désactivée sur ce serveur." }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); + } + } + if (command.dm !== true && message.channel.type === 1) return message .reply({ diff --git a/app/events/executeCommands/slash.js b/app/events/executeCommands/slash.js index b97d4c0..41c5d3c 100644 --- a/app/events/executeCommands/slash.js +++ b/app/events/executeCommands/slash.js @@ -1,4 +1,3 @@ - module.exports = { name: "interactionCreate", async execute(client, interaction) { @@ -7,6 +6,24 @@ module.exports = { const command = client.commands.get(interaction.commandName); if (!command) return; + // Vérification du scope / guildCondition + if (command.scope === "guild") { + const guildId = interaction.guild ? interaction.guild.id : null; + if (!guildId) + return interaction.reply({ content: "Cette commande ne peut pas être utilisée en message privé.", ephemeral: true }); + if (typeof command.guildCondition === "function") { + let allowed = false; + try { + allowed = await command.guildCondition(guildId); + } catch (err) { + console.error(`Erreur guildCondition pour la guild ${guildId}`, err); + allowed = false; + } + if (!allowed) + return interaction.reply({ content: "Cette commande est désactivée sur ce serveur.", ephemeral: true }); + } + } + if (command.dm !== true && interaction.channel.type === 1) return interaction.reply({ content: "Cette commande ne peut pas être utilisée en message privé.", diff --git a/app/fonctions/addCommand.js b/app/fonctions/addCommand.js index c9b7276..634c17d 100644 --- a/app/fonctions/addCommand.js +++ b/app/fonctions/addCommand.js @@ -43,8 +43,8 @@ function addCommand({ // Permissions let defaultMemberPermissions = null; if (permissions.length > 0) { - defaultMemberPermissions = new PermissionsBitField(); - permissions.forEach(p => defaultMemberPermissions.add(p)); + // Résoudre en bitfield (BigInt) compatible avec setDefaultMemberPermissions + defaultMemberPermissions = PermissionsBitField.resolve(permissions); } // Création du SlashCommandBuilder @@ -58,14 +58,22 @@ function addCommand({ slashOptions.forEach(opt => { switch (opt.type) { case "STRING": - slashData.addStringOption(o => - o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) - ); + slashData.addStringOption(o => { + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required); + if (opt.choices && Array.isArray(opt.choices)) { + o.addChoices(...opt.choices); + } + return o; + }); break; case "INTEGER": - slashData.addIntegerOption(o => - o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required) - ); + slashData.addIntegerOption(o => { + o.setName(opt.name).setDescription(opt.description || "No description").setRequired(!!opt.required); + if (opt.choices && Array.isArray(opt.choices)) { + o.addChoices(...opt.choices); + } + return o; + }); break; case "BOOLEAN": slashData.addBooleanOption(o => diff --git a/app/public/guild.html b/app/public/guild.html index b8ca408..c69d409 100644 --- a/app/public/guild.html +++ b/app/public/guild.html @@ -263,6 +263,103 @@
+ + +
+

💰 Système d'économie

+ + + + + + + + + +

📅 Récompense quotidienne

+ + + + +

💼 Travail

+ + + + + + +

🔫 Crime

+ + + + + + + + + + + +
+
+ + + @@ -271,5 +368,6 @@ + diff --git a/app/public/guild/economyForm.js b/app/public/guild/economyForm.js new file mode 100644 index 0000000..3f1170a --- /dev/null +++ b/app/public/guild/economyForm.js @@ -0,0 +1,68 @@ +const economyForm = document.getElementById("economy-form"); +const economyEnabled = document.getElementById("economy-enabled"); +const currencyName = document.getElementById("economy-currency-name"); +const currencySymbol = document.getElementById("economy-currency-symbol"); +const startingBalance = document.getElementById("economy-starting-balance"); +const dailyAmount = document.getElementById("economy-daily-amount"); +const dailyCooldown = document.getElementById("economy-daily-cooldown"); +const workMin = document.getElementById("economy-work-min"); +const workMax = document.getElementById("economy-work-max"); +const workCooldown = document.getElementById("economy-work-cooldown"); +const crimeMin = document.getElementById("economy-crime-min"); +const crimeMax = document.getElementById("economy-crime-max"); +const crimeSuccess = document.getElementById("economy-crime-success"); +const crimeFine = document.getElementById("economy-crime-fine"); +const crimeCooldown = document.getElementById("economy-crime-cooldown"); +const statusEconomyForm = document.getElementById("status-economy-form"); + +// Charger la config existante +fetch(`/api/bot/get-economy-config/${guildId}`) + .then(res => res.json()) + .then(cfg => { + economyEnabled.checked = cfg.enabled; + currencyName.value = cfg.currencyName; + currencySymbol.value = cfg.currencySymbol; + startingBalance.value = cfg.startingBalance; + dailyAmount.value = cfg.dailyAmount; + dailyCooldown.value = cfg.dailyCooldownHours; + workMin.value = cfg.workMinAmount; + workMax.value = cfg.workMaxAmount; + workCooldown.value = cfg.workCooldownMinutes; + crimeMin.value = cfg.crimeMinAmount; + crimeMax.value = cfg.crimeMaxAmount; + crimeSuccess.value = cfg.crimeSuccessRate; + crimeFine.value = cfg.crimeFinePercent; + crimeCooldown.value = cfg.crimeCooldownMinutes; + }) + .catch(console.error); + +// Sauvegarder la config +economyForm.addEventListener("submit", async e => { + e.preventDefault(); + + const res = await fetch("/api/bot/save-economy-config", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + guildId, + economyEnabled: economyEnabled.checked, + currencyName: currencyName.value, + currencySymbol: currencySymbol.value, + startingBalance: parseInt(startingBalance.value, 10), + dailyAmount: parseInt(dailyAmount.value, 10), + dailyCooldownHours: parseInt(dailyCooldown.value, 10), + workMinAmount: parseInt(workMin.value, 10), + workMaxAmount: parseInt(workMax.value, 10), + workCooldownMinutes: parseInt(workCooldown.value, 10), + crimeMinAmount: parseInt(crimeMin.value, 10), + crimeMaxAmount: parseInt(crimeMax.value, 10), + crimeSuccessRate: parseInt(crimeSuccess.value, 10), + crimeFinePercent: parseInt(crimeFine.value, 10), + crimeCooldownMinutes: parseInt(crimeCooldown.value, 10) + }) + }); + + statusEconomyForm.textContent = (await res.json()).success + ? "Config économie sauvegardée ✅" + : "Erreur ❌"; +}); diff --git a/app/routes/api.js b/app/routes/api.js index 6c543b9..24bedb4 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -548,5 +548,122 @@ module.exports = (app, db, client) => { ); }); + + // ============== ECONOMY CONFIG ============== + + router.get("/bot/get-economy-config/:guildId", (req, res) => { + const { guildId } = req.params; + + db.get( + `SELECT * FROM economy_config WHERE guild_id = ?`, + [guildId], + (err, row) => { + if (err || !row) { + return res.json({ + enabled: false, + currencyName: "coins", + currencySymbol: "💰", + dailyAmount: 100, + dailyCooldownHours: 24, + workMinAmount: 50, + workMaxAmount: 150, + workCooldownMinutes: 60, + crimeMinAmount: 100, + crimeMaxAmount: 500, + crimeSuccessRate: 50, + crimeFinePercent: 30, + crimeCooldownMinutes: 120, + startingBalance: 0 + }); + } + res.json({ + enabled: !!row.enabled, + currencyName: row.currency_name, + currencySymbol: row.currency_symbol, + dailyAmount: row.daily_amount, + dailyCooldownHours: row.daily_cooldown_hours, + workMinAmount: row.work_min_amount, + workMaxAmount: row.work_max_amount, + workCooldownMinutes: row.work_cooldown_minutes, + crimeMinAmount: row.crime_min_amount, + crimeMaxAmount: row.crime_max_amount, + crimeSuccessRate: row.crime_success_rate, + crimeFinePercent: row.crime_fine_percent, + crimeCooldownMinutes: row.crime_cooldown_minutes, + startingBalance: row.starting_balance + }); + } + ); + }); + + router.post("/bot/save-economy-config", express.json(), (req, res) => { + const { + guildId, + economyEnabled, + currencyName, + currencySymbol, + dailyAmount, + dailyCooldownHours, + workMinAmount, + workMaxAmount, + workCooldownMinutes, + crimeMinAmount, + crimeMaxAmount, + crimeSuccessRate, + crimeFinePercent, + crimeCooldownMinutes, + startingBalance + } = req.body; + + if (!req.session.guilds) { + return res.status(401).json({ success: false }); + } + + const isAdmin = req.session.guilds.find( + g => g.id === guildId && (BigInt(g.permissions) & 0x8n) === 0x8n + ); + + if (!isAdmin) { + return res.status(403).json({ success: false }); + } + + db.run( + `INSERT INTO economy_config ( + guild_id, enabled, currency_name, currency_symbol, + daily_amount, daily_cooldown_hours, + work_min_amount, work_max_amount, work_cooldown_minutes, + crime_min_amount, crime_max_amount, crime_success_rate, crime_fine_percent, crime_cooldown_minutes, + starting_balance + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(guild_id) DO UPDATE SET + enabled = ?, currency_name = ?, currency_symbol = ?, + daily_amount = ?, daily_cooldown_hours = ?, + work_min_amount = ?, work_max_amount = ?, work_cooldown_minutes = ?, + crime_min_amount = ?, crime_max_amount = ?, crime_success_rate = ?, crime_fine_percent = ?, crime_cooldown_minutes = ?, + starting_balance = ?`, + [ + guildId, + economyEnabled ? 1 : 0, currencyName, currencySymbol, + dailyAmount, dailyCooldownHours, + workMinAmount, workMaxAmount, workCooldownMinutes, + crimeMinAmount, crimeMaxAmount, crimeSuccessRate, crimeFinePercent, crimeCooldownMinutes, + startingBalance, + economyEnabled ? 1 : 0, currencyName, currencySymbol, + dailyAmount, dailyCooldownHours, + workMinAmount, workMaxAmount, workCooldownMinutes, + crimeMinAmount, crimeMaxAmount, crimeSuccessRate, crimeFinePercent, crimeCooldownMinutes, + startingBalance + ], + err => { + if (err) { + console.error(err); + return res.status(500).json({ success: false }); + } + loadSlashCommands(client, guildId); + res.json({ success: true }); + } + ); + }); + app.use("/api", router); };