├── utils ├── GetUserFromMention.js ├── SaveObjToDB.js ├── ReadDBFile.js ├── ClassFromName.js ├── GetClassString.js ├── UserCheck.js ├── RegisterUser.js ├── TimeDiff.js └── FindCardByName.js ├── package.json ├── constants ├── constants.js └── locales.js ├── LICENSE ├── .gitignore ├── commands ├── Undiscovered.js ├── ActivateCode.js ├── DeleteCard.js ├── ResetDrop.js ├── CreateCode.js ├── AddNewCard.js ├── EditCard.js ├── GiveCard.js ├── Profile.js ├── DropCard.js └── ShowCards.js ├── index.js ├── README.md └── RU-README.md /utils/GetUserFromMention.js: -------------------------------------------------------------------------------- 1 | function getUserFromMention(mention) { 2 | const matches = mention.match(/^<@!?(\d+)>$/); 3 | if (!matches) return; 4 | return matches[1]; // user id 5 | } 6 | module.exports = getUserFromMention; -------------------------------------------------------------------------------- /utils/SaveObjToDB.js: -------------------------------------------------------------------------------- 1 | /* 2 | Saves database object in file systew 3 | */ 4 | const fs = require('fs'); 5 | module.exports = function SaveObjToDB(obj) { 6 | fs.writeFileSync('./storage/db.json', JSON.stringify(obj, null, "\t"), 'utf8'); 7 | } -------------------------------------------------------------------------------- /utils/ReadDBFile.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | 3 | module.exports = function ReadDBFile() { 4 | let obj; 5 | try { 6 | let f = fs.readFileSync('./storage/db.json', 'utf8'); 7 | obj = JSON.parse(f); 8 | } catch (e) { 9 | return undefined; 10 | } 11 | return obj 12 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discordcardbot", 3 | "version": "0.0.1", 4 | "description": "giving a random card per day", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "discordjs" 12 | ], 13 | "author": "Lexa307", 14 | "license": "MIT", 15 | "dependencies": { 16 | "discord.js": "^14.24.2", 17 | "dotenv": "^10.0.0", 18 | "uuid": "^8.3.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utils/ClassFromName.js: -------------------------------------------------------------------------------- 1 | function replaceEmojisFromNameToClass(card) { 2 | let regexp = new RegExp(/()?/g); 3 | let result = [...card.name.matchAll(regexp)]; 4 | let cardClassString = ""; 5 | if(result.length) { 6 | for (let match of result) { 7 | cardClassString+= match; 8 | } 9 | cardClassString = cardClassString.replace(/,/g, ""); 10 | card.name = card.name.replace(regexp, "" ); 11 | return cardClassString; 12 | } 13 | return cardClassString; 14 | } 15 | 16 | module.exports = replaceEmojisFromNameToClass -------------------------------------------------------------------------------- /utils/GetClassString.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../constants/constants"); 2 | 3 | function GetClassString (cardClass) { 4 | let cardClassString = ""; 5 | let fillCount; 6 | if (cardClass <= CONSTANTS.RARE_CLASS_NUMBER) { 7 | for (fillCount = 0; fillCount < cardClass; fillCount++) { 8 | cardClassString += CONSTANTS.CLASS_SYMBOL_FILL; 9 | } 10 | 11 | for (fillCount; fillCount < CONSTANTS.RARE_CLASS_NUMBER; fillCount++) { 12 | cardClassString += CONSTANTS.CLASS_SYMBOL_OF_VOID; 13 | } 14 | } 15 | return cardClassString; 16 | } 17 | 18 | module.exports = GetClassString; -------------------------------------------------------------------------------- /utils/UserCheck.js: -------------------------------------------------------------------------------- 1 | const RegisterUser = require('./RegisterUser.js'); 2 | const ReadDBFile = require("./ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const LOCALES = require('../constants/locales.js'); 5 | const UserMemberCheck = (user) => { 6 | let obj = ReadDBFile(); 7 | console.log(user); 8 | if (obj.users) { // if DB file exist 9 | let finded = false; 10 | for (let i of obj.users ) { 11 | if(i.id == user) { 12 | finded = true; 13 | return i; 14 | } 15 | } 16 | /*if (!finded)*/ 17 | RegisterUser(user, obj); 18 | return false; 19 | } else { 20 | console.error(LOCALES.UserCheck__MessageEmbed_db_error[CONSTANTS.LANG]); 21 | return false; 22 | } 23 | }; 24 | module.exports = UserMemberCheck; 25 | -------------------------------------------------------------------------------- /constants/constants.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const INVENTORY_TIME = process.env.INVENTORY_TIME; 3 | const CLASS_SYMBOL_FILL = process.env.CLASS_SYMBOL_FILL; 4 | const CLASS_SYMBOL_OF_VOID = process.env.CLASS_SYMBOL_OF_VOID; 5 | const RARE_CLASS_NUMBER = process.env.RARE_CLASS_NUMBER; 6 | const PAGE_SIZE = process.env.PAGE_SIZE; 7 | const PREFIX = process.env.PREFIX; 8 | const TOKEN = process.env.TOKEN; 9 | const INVENTORY_PUBLIC_ACCESS = parseInt(process.env.INVENTORY_PUBLIC_ACCESS); 10 | const MOD_ROLE_NAME = process.env.MOD_ROLE_NAME; 11 | const RESET_LOCAL_TIME = (process.env.RESET_LOCAL_TIME).split(':'); 12 | const LANG = process.env.LOCALES; 13 | const SPACE_REGEX = process.env.SPACE_SYMBOL //new RegExp(`${process.env.SPACE_SYMBOL}/g`); 14 | console.log(SPACE_REGEX); 15 | module.exports = { 16 | INVENTORY_TIME, 17 | RESET_LOCAL_TIME, 18 | CLASS_SYMBOL_FILL, 19 | CLASS_SYMBOL_OF_VOID, 20 | RARE_CLASS_NUMBER, 21 | PAGE_SIZE, 22 | LANG, 23 | PREFIX, 24 | INVENTORY_PUBLIC_ACCESS, 25 | SPACE_REGEX, 26 | TOKEN 27 | } -------------------------------------------------------------------------------- /utils/RegisterUser.js: -------------------------------------------------------------------------------- 1 | const ReadDBFile = require("./ReadDBFile.js"); 2 | const SaveObjToDB = require("./SaveObjToDB.js"); 3 | const LOCALES = require("../constants/locales.js"); 4 | const CONSTANTS = require("../constants/constants.js"); 5 | /* User db.json reference 6 | User { 7 | users: [ 8 | { 9 | id: Discord_id, 10 | cards: 11 | [ 12 | { 13 | name: String, 14 | count: Number, 15 | url: String 16 | } 17 | ], 18 | lastDropDate : Date 19 | } 20 | ] 21 | 22 | } 23 | */ 24 | 25 | const RegisterUser = (user, dbObj = ReadDBFile()) => { 26 | if(!dbObj) return; 27 | dbObj.users.push( 28 | { 29 | id: user, 30 | cards: [], 31 | lastDropDate: null 32 | } 33 | ) 34 | SaveObjToDB(dbObj); 35 | console.log(`${user} ${LOCALES.RegisterUser__MessageEmbed_registered[CONSTANTS.LANG]}`) 36 | } 37 | 38 | module.exports = RegisterUser; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Mr Bruh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/TimeDiff.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require('../constants/constants.js'); 2 | const ReadDBFile = require("./ReadDBFile.js"); 3 | const fs = require('fs'); 4 | let dailyInterval; 5 | 6 | function ReturnRequeredUTCDateToReset () { 7 | const configLocalTime = CONSTANTS.RESET_LOCAL_TIME; 8 | let localDate = new Date(); 9 | localDate.setHours(parseInt(configLocalTime[0]), parseInt(configLocalTime[1]), parseInt(configLocalTime[2]|0), 0); 10 | if (new Date() > localDate) localDate.setDate(localDate.getDate() + 1); 11 | return localDate; 12 | } 13 | 14 | function ResetByInterval() { 15 | clearInterval(dailyInterval); 16 | let obj = ReadDBFile(); 17 | for (let i of obj.users ) { 18 | i.lastDropDate = null; 19 | } 20 | let json = JSON.stringify(obj, null, "\t"); 21 | fs.writeFileSync('./storage/db.json', json, 'utf8'); 22 | dailyInterval = setInterval( () => {ResetByInterval()}, ReturnRequeredUTCDateToReset() - new Date()); 23 | } 24 | 25 | if (CONSTANTS.RESET_LOCAL_TIME[0]) { 26 | dailyInterval = setInterval( () => {ResetByInterval()}, ReturnRequeredUTCDateToReset() - new Date()); 27 | } 28 | 29 | module.exports = ReturnRequeredUTCDateToReset -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | storage/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | -------------------------------------------------------------------------------- /utils/FindCardByName.js: -------------------------------------------------------------------------------- 1 | const CONSTANTS = require("../constants/constants"); 2 | const ReadDBFile = require("./ReadDBFile.js"); 3 | const LOCALES = require('../constants/locales.js'); 4 | function GetCardObjectByName (message, cardname, strict = false) { 5 | // returns a single card object that can be found by a single match of its name in the database 6 | // if it has greater than 1 matches or zero it will ask user to write exactly name of card that should be find 7 | const obj = ReadDBFile(); 8 | const matches = []; 9 | for (let i = 0; i < obj.cards.length; i++) { 10 | if ( (!strict) ? ~obj.cards[i].name.toLowerCase().indexOf(cardname.toLowerCase()) : obj.cards[i].name.toLowerCase() == cardname.toLowerCase() ) matches.push(obj.cards[i]); 11 | } 12 | if (matches.length == 1) return matches[0]; 13 | if (!strict) { 14 | if (matches.length > 1) message.reply(`${LOCALES.FindCardByName__MessageEmbed_one_more_card_exist[CONSTANTS.LANG]}`); 15 | if (matches.length == 0) message.reply(`${LOCALES.FindCardByName__MessageEmbed_no_similar_name_found[CONSTANTS.LANG]}`); 16 | } 17 | return 0; // returns zero if results of searching was incorrect. 18 | } 19 | 20 | module.exports = GetCardObjectByName; -------------------------------------------------------------------------------- /commands/Undiscovered.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const LOCALES = require('../constants/locales.js'); 5 | 6 | const NoOneElseHas = (message, args, client) => { 7 | UserCheck(message.author.id); 8 | let obj = ReadDBFile(); 9 | let undiscoveredCounter = 0; 10 | if (!obj.cards.length) { 11 | message.reply(`${LOCALES.Undiscovered__MessageEmbed__no_cards_in_base[CONSTANTS.LANG]}`); 12 | return; 13 | } 14 | if (!obj.users.length) { 15 | message.reply(`${LOCALES.Undiscovered__MessageEmbed__no_users[CONSTANTS.LANG]}`); 16 | return; 17 | } 18 | 19 | for (let card of obj.cards) { 20 | 21 | let finded = false; 22 | let usr = obj.users.find( user => { return user.cards.find( c => { return c.name == card.name } ) }); 23 | 24 | (!!usr) ? finded = true : undiscoveredCounter++ 25 | } 26 | message.reply(`${LOCALES.Undiscovered__MessageEmbed__cards_untouched[CONSTANTS.LANG]}**${undiscoveredCounter}** `) 27 | } 28 | 29 | module.exports = { 30 | name: `${LOCALES.Undiscovered__EXPORTS__name[CONSTANTS.LANG]}`, 31 | usage() { return `${CONSTANTS.PREFIX}${this.name}`; }, 32 | desc: `${LOCALES.Undiscovered__EXPORTS__desc[CONSTANTS.LANG]}`, 33 | func: NoOneElseHas, 34 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Client, EmbedBuilder, PermissionsBitField } = require('discord.js'); 2 | const fs = require('fs'); 3 | const client = new Client({intents: ['Guilds', 'GuildMessages', 'MessageContent', 'GuildMessageReactions']}); 4 | const CONSTANTS = require('./constants/constants.js'); 5 | const UserCheck = require("./utils/UserCheck.js"); 6 | const LOCALES = require("./constants/locales.js"); 7 | const BOT_COMMANDS = []; 8 | BOT_COMMANDS.push( 9 | { 10 | name: `${LOCALES.Help__EXPORTS__name[CONSTANTS.LANG]}`, 11 | usage() { return `${CONSTANTS.PREFIX}${this.name}`; }, 12 | desc: `${LOCALES.Help__EXPORTS__desc[CONSTANTS.LANG]}`, 13 | func(message) { 14 | const helpEmbed = new EmbedBuilder() 15 | .setTitle(`${LOCALES.Help__MessageEmbed_commands[CONSTANTS.LANG]}`) 16 | .setColor('#84cc64'); 17 | for (let i = 0; i < BOT_COMMANDS.length; i++) { 18 | if ( (!message.member.permissions.has(PermissionsBitField.Flags.Administrator) && !BOT_COMMANDS[i].permission) || message.member.permissions.has(PermissionsBitField.Flags.Administrator) ) 19 | helpEmbed.addFields({name: BOT_COMMANDS[i].usage(), value: BOT_COMMANDS[i].desc, inline: false}); 20 | } 21 | message.channel.send({embeds: [helpEmbed]}); 22 | }, 23 | }, 24 | ); 25 | 26 | client.on('clientReady', () => { 27 | console.log('Ready'); 28 | console.log(client.generateInvite({scopes: ['bot']})) 29 | }); 30 | 31 | client.on('guildMemberAdd', member => { 32 | UserCheck(member.user.id); 33 | }); 34 | 35 | fs.readdir(`${__dirname}/commands`, (err, file) => { 36 | for (let i = 0; i < file.length; i++) { 37 | BOT_COMMANDS.push(require(`./commands/${file[i]}`)); 38 | } 39 | }); 40 | 41 | client.on('messageCreate', message => { 42 | if (! message.content.startsWith(CONSTANTS.PREFIX)) return ; 43 | const args = message.content.slice(CONSTANTS.PREFIX.length).split(/ +/g); 44 | const command = args.shift().toLowerCase(); 45 | const cmd = BOT_COMMANDS.find( botcommand => botcommand.name == command ); 46 | console.log(cmd); 47 | if (cmd) cmd.func(message, args, client); 48 | }); 49 | client.login(CONSTANTS.TOKEN); -------------------------------------------------------------------------------- /commands/ActivateCode.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const SaveObjToDB = require('../utils/SaveObjToDB.js'); 5 | const LOCALES = require('../constants/locales.js'); 6 | 7 | function ActivateCode (message, args, client) { 8 | UserCheck(message.author.id); 9 | if (args.length > 0) { 10 | let obj = ReadDBFile(); 11 | // check code if exists 12 | let userCode = args.join(' '); //user can ativate code using default space 13 | let dbCode = obj.codes.find(elem => {return elem.code === userCode }) 14 | if (dbCode.active) { 15 | // check date restrictions of code 16 | if (dbCode.expDate) { 17 | if ((new Date().getTime() - new Date(dbCode.expDate).getTime() ) > 0) { 18 | message.reply(LOCALES.ActivateCode__MessageEmbed__code_expired[CONSTANTS.LANG]); 19 | return; 20 | } 21 | } 22 | // check code for avalible usage count 23 | if (dbCode.usingCount) { 24 | if (dbCode.usingCount <= dbCode.usedBy.length) { 25 | message.reply(LOCALES.ActivateCode__MessageEmbed__exceeded_number_uses[CONSTANTS.LANG]); 26 | return; 27 | } 28 | } 29 | 30 | // check if user already used this code 31 | if (dbCode.usedBy.find(elem => { return elem.userId == message.author.id})) { 32 | message.reply(LOCALES.ActivateCode__MessageEmbed__already_used[CONSTANTS.LANG]); 33 | return; 34 | } 35 | 36 | // activate code for user 37 | dbCode.usedBy.push({ 38 | "userId" : message.author.id, 39 | "date": new Date() 40 | }) 41 | 42 | let user = obj.users.find((usr) => {if (usr.id == message.author.id) return usr}) 43 | user.lastDropDate = null; 44 | SaveObjToDB(obj); 45 | message.reply(`${LOCALES.ActivateCode__MessageEmbed__code[CONSTANTS.LANG]}**${userCode}**${LOCALES.ActivateCode__MessageEmbed__activated[CONSTANTS.LANG]}`); 46 | 47 | } 48 | 49 | } 50 | } 51 | 52 | module.exports = { 53 | name: `${LOCALES.ActivateCode__EXPORTS__name[CONSTANTS.LANG]}`, 54 | usage() { return `${CONSTANTS.PREFIX}${this.name} Code `; }, 55 | desc: `${LOCALES.ActivateCode__EXPORTS__desc[CONSTANTS.LANG]}`, 56 | func: ActivateCode, 57 | }; -------------------------------------------------------------------------------- /commands/DeleteCard.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const FindCardByName = require('../utils/FindCardByName.js'); 5 | const GetClassString = require("../utils/GetClassString.js"); 6 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 7 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 8 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 9 | const LOCALES = require('../constants/locales.js'); 10 | 11 | function showDeletedCard(message, card, obj, client) { 12 | let cardClassNumber = card.class; 13 | let cardClassString = GetClassString(cardClassNumber); 14 | client.users.fetch(message.author.id).then(user => { 15 | let embed = new EmbedBuilder(); 16 | embed.setColor("#d1b91f"); 17 | embed.setAuthor({name: user.username, iconURL: user.displayAvatarURL(), url: user.url}); 18 | embed.setTitle(`${LOCALES.DeleteCard__MessageEmbed__deleted_card_with_name[CONSTANTS.LANG]}`); 19 | embed.setDescription(`**${(cardClassString) ? cardClassString : ReplaceEmojisFromNameToClass(card)} [${card.name}](${card.url})**`); 20 | embed.setImage(`${card.url}`); 21 | message.reply({embeds: [embed]}); 22 | }); 23 | } 24 | 25 | function DeleteCard (message, args, client) { 26 | UserCheck(message.author.id); 27 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; //this command can use admin only 28 | let obj = ReadDBFile(); 29 | if (args.length == 1) { //strong delete 30 | let deleteCardName = args[0]; 31 | deleteCardName = deleteCardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); // "SPACE_SYMBOL" should use as ' ' if you want to add space in cardname 32 | if (!FindCardByName(message, deleteCardName, true)) {message.reply(`${LOCALES.DeleteCard__MessageEmbed__card_not_found[CONSTANTS.LANG]}`); return; } 33 | const DeleteCard = obj.cards.find(card => { if(card.name == deleteCardName) return card});// get reference of object to edit and save to DB after all 34 | // delete card in card pool 35 | if (DeleteCard) obj.cards = obj.cards.filter(function(item) { return item !== DeleteCard }) 36 | 37 | for (let usr of obj.users) { //delete card from all users 38 | usr.cards = usr.cards.filter(function(item) { return item.name !== DeleteCard.name }) 39 | } 40 | SaveObjToDB(obj); 41 | showDeletedCard(message, DeleteCard, obj, client); 42 | } else { 43 | message.reply(`${LOCALES.DeleteCard__MessageEmbed__mandatory_argument[CONSTANTS.LANG]}`); 44 | return; 45 | } 46 | } 47 | 48 | module.exports = { 49 | name: `${LOCALES.DeleteCard__EXPORTS__name[CONSTANTS.LANG]}`, 50 | usage() { return `${CONSTANTS.PREFIX}${this.name} CardName` }, 51 | desc: `${LOCALES.DeleteCard__EXPORTS__desc[CONSTANTS.LANG]}`, 52 | func: DeleteCard, 53 | permission: 'ADMINISTRATOR' 54 | }; -------------------------------------------------------------------------------- /commands/ResetDrop.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const GetUserFromMention = require("../utils/GetUserFromMention.js"); 5 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 6 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 7 | const LOCALES = require('../constants/locales.js'); 8 | 9 | function ResetDrop (message, args, client) { 10 | UserCheck(message.author.id); 11 | if (!message.guild) return; //if message is not DM 12 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; //this command can use admin only 13 | let member, messageEnding; 14 | if (args[0]) { 15 | member = GetUserFromMention(args[0]); 16 | if (!member) { 17 | message.channel.send(`${LOCALES.ResetDrop__MessageEmbed__specify_user[CONSTANTS.LANG]}`); 18 | return; 19 | } 20 | client.users.fetch(member).then(user => { 21 | messageEnding = user.username; 22 | }); 23 | } else { 24 | member = null; 25 | messageEnding = `${LOCALES.ResetDrop__MessageEmbed__to_all_users[CONSTANTS.LANG]}`; 26 | } 27 | UserCheck(member); 28 | const memeGifs = [ // TODO: comment this at master 29 | // "https://c.tenor.com/hJR5o7BTK7MAAAAC/serial-experiments-lain-lain.gif", 30 | // "https://c.tenor.com/PaeXns-85fQAAAAC/scp.gif", 31 | // "https://c.tenor.com/33anXDfc5mUAAAAd/coconut-nekopara.gif", 32 | // "https://c.tenor.com/7-gvhGIXMNkAAAAd/metal-gear-rising-jetstream-sam.gif", 33 | // "https://c.tenor.com/fws6rIp681UAAAAd/gman-walk.gif", 34 | // "https://c.tenor.com/KHpeP1HyGOYAAAAC/mouse-dance.gif", 35 | // "https://c.tenor.com/HPKNtgcxC0YAAAAd/gachi.gif", 36 | // "https://c.tenor.com/_d9UdsUv8jIAAAAC/congratulations-evangelion.gif", 37 | // "https://c.tenor.com/-7iAbYi5EdIAAAAd/memes-meme.gif", 38 | // "https://c.tenor.com/Zd4fex5jsoYAAAAC/american-psycho-patrick-bateman.gif", 39 | // "https://c.tenor.com/CO2IhmW-do8AAAAd/yandere-dev-milk.gif" 40 | "https://media.tenor.com/6k-ayhy51NAAAAAd/gachi-flew.gif" 41 | ] 42 | 43 | const obj = ReadDBFile(); 44 | 45 | if (!member) { 46 | for (let user of obj.users) { 47 | user.lastDropDate = null; 48 | } 49 | } else { 50 | let user = obj.users.find((usr) => {if (usr.id == member) return usr}) 51 | user.lastDropDate = null; 52 | } 53 | SaveObjToDB(obj); 54 | client.users.fetch(message.author.id).then(user => { 55 | let embed = new EmbedBuilder(); 56 | embed.setColor("#d1b91f"); 57 | embed.setAuthor({name: user.username, iconURL: user.displayAvatarURL(), url: user.url}); 58 | embed.setTitle(`${LOCALES.ResetDrop__MessageEmbed__updated_drops[CONSTANTS.LANG]}${messageEnding}`); 59 | embed.setImage(memeGifs[Math.floor(Math.random() * memeGifs.length)]); // So i decided to add some funny/congrats gifs that will be shown in chat, be free to add something special for your users or to disable it just // this string 60 | message.reply({embeds: [embed]}); 61 | }) 62 | } 63 | 64 | module.exports = { 65 | name: `${LOCALES.ResetDrop__EXPORTS__name[CONSTANTS.LANG]}`, 66 | usage() { return `${CONSTANTS.PREFIX}${this.name} [@UserMention]`; }, 67 | desc: `${LOCALES.ResetDrop__EXPORTS__desc[CONSTANTS.LANG]}`, 68 | func: ResetDrop, 69 | permission: 'ADMINISTRATOR' 70 | }; -------------------------------------------------------------------------------- /commands/CreateCode.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const SaveObjToDB = require('../utils/SaveObjToDB.js'); 5 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 6 | const { v4: uuidv4, v4 } = require('uuid'); 7 | const LOCALES = require("../constants/locales.js"); 8 | 9 | 10 | function showCreatedCode(message, code, client) { 11 | client.users.fetch(message.author.id).then(user => { 12 | let embed = new EmbedBuilder(); 13 | embed.setColor("#d1b91f"); 14 | embed.setAuthor({name: user.username, iconURL: user.displayAvatarURL(), url: user.url}); 15 | embed.setTitle(`${LOCALES.CreateCode__MessageEmbed__created_code_with_name[CONSTANTS.LANG]}${code.code} `); 16 | embed.setDescription(` 17 | ${LOCALES.CreateCode__MessageEmbed__able_to_use_it[CONSTANTS.LANG]}${(code.usingCount) ? `**${code.usingCount}**` : `${LOCALES.CreateCode__MessageEmbed__unlimited_quantity[CONSTANTS.LANG]}`} ${LOCALES.CreateCode__MessageEmbed__users[CONSTANTS.LANG]} 18 | ${LOCALES.CreateCode__MessageEmbed__code_expiration_date[CONSTANTS.LANG]}${(code.expDate) ? code.expDate : LOCALES.CreateCode__MessageEmbed__just_unlimited[CONSTANTS.LANG] } 19 | 20 | `); 21 | embed.setImage(`https://c.tenor.com/YdLPqVX9RVoAAAAi/klee-genshin.gif`); //pls change that gif to something normal 22 | message.reply({embeds: [embed]}); 23 | }); 24 | } 25 | 26 | function CreateNewCode (message, args, client) { 27 | UserCheck(message.author.id); 28 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; //this command can use admin only 29 | let defaultCode = { 30 | "code": uuidv4(), 31 | "createdBy": message.author.username, 32 | "timeStamp": new Date(), 33 | "usingCount": 1, 34 | "expDate": null, 35 | "active": true, 36 | "usedBy": [], //{user, date} 37 | } 38 | let obj = ReadDBFile(); 39 | if (!obj.codes) obj.codes = []; 40 | // lets pars specific parameters 41 | if (args.length != 0) { 42 | let usingCount = args.find(elem => {return `${elem}`.startsWith('-u')}); 43 | let expDate = args.find(elem => {return `${elem}`.startsWith('-d')}); 44 | let code = args.find(elem => {return `${elem}`.startsWith('-c')}); 45 | 46 | // Check usingCount 47 | if (usingCount) { 48 | usingCount = usingCount.replace("-u", ''); 49 | if (usingCount.matchAll('^\d+$')) { 50 | defaultCode.usingCount = parseInt(usingCount, 10); 51 | } 52 | } 53 | 54 | // Check usingCount 55 | if (expDate) { 56 | expDate = expDate.replace("-d", ''); 57 | if (usingCount.matchAll('/^\d{2}[./-]\d{2}[./-]\d{4}$/')) { 58 | defaultCode.expDate = new Date(expDate); 59 | } 60 | } 61 | 62 | if (code) { 63 | code = code.replace("-c", ''); 64 | code = code.replaceAll(CONSTANTS.SPACE_REGEX, ' '); 65 | defaultCode.code = code; 66 | } 67 | } 68 | 69 | obj.codes.push(defaultCode); 70 | SaveObjToDB(obj); 71 | showCreatedCode(message, defaultCode, client); 72 | 73 | 74 | } 75 | 76 | module.exports = { 77 | name: `${LOCALES.CreateCode__EXPORTS__name[CONSTANTS.LANG]}`, 78 | usage() { return `${CONSTANTS.PREFIX}${this.name} -c[CodeName] -d[exp-date] -u[usingCount]` }, 79 | desc: `${LOCALES.CreateCode__EXPORTS__desc[CONSTANTS.LANG]}`, 80 | func: CreateNewCode, 81 | permission: 'ADMINISTRATOR' 82 | }; -------------------------------------------------------------------------------- /commands/AddNewCard.js: -------------------------------------------------------------------------------- 1 | /*summary: adds new card to drop pull 2 | 3 | example: !новаякарта Королева;монстров 6 https://media.discordapp.net/attachments/852679774128439386/993195205731819559/117.gif 4 | ";" - using as " " to avoid errors on spliting args 5 | also you can skip 4th argument by adding attachment to your message 6 | */ 7 | const UserCheck = require("../utils/UserCheck.js"); 8 | const ReadDBFile = require("../utils/ReadDBFile.js"); 9 | const CONSTANTS = require ("../constants/constants.js"); 10 | const FindCardByName = require('../utils/FindCardByName.js'); 11 | const GetClassString = require("../utils/GetClassString.js"); 12 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 13 | const SaveObjToDB = require('../utils/SaveObjToDB.js'); 14 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 15 | const LOCALES = require('../constants/locales.js'); 16 | 17 | function attachIsImage(msgAttach) { 18 | let url = msgAttach.url; 19 | //True if this url is a png/jpg/gif image. 20 | return url.indexOf("png", url.length - 3) !== -1 || url.indexOf("jpg", url.length - 3) !== -1 || url.indexOf("gif", url.length - 3) !== -1 || url.indexOf("mp4", url.length - 3); 21 | } 22 | 23 | function isUrlMp4(url) { 24 | return url.indexOf("mp4", url.length - 3) !== -1; 25 | } 26 | 27 | function showNewCard(message, card, obj, client) { 28 | let cardClassNumber = obj.cards.find(cardDB => {return cardDB.name == card.name}).class; 29 | let cardClassString = GetClassString(cardClassNumber); 30 | client.users.fetch(message.author.id).then(user => { 31 | let embed = new EmbedBuilder(); 32 | embed.setColor("#d1b91f"); 33 | embed.setAuthor({name: user.username, iconURL: user.displayAvatarURL(), url: user.url}); 34 | embed.setTitle(`${LOCALES.AddNewCard__MessageEmbed__added_card_with_name[CONSTANTS.LANG]}`); 35 | embed.setDescription(`**${(cardClassString) ? cardClassString : ReplaceEmojisFromNameToClass(card)} [${card.name}](${card.url})**`); 36 | embed.setImage(`${card.url}`); 37 | message.reply({embeds: [embed]}); 38 | }); 39 | } 40 | 41 | function AddNewCard (message, args, client) { 42 | UserCheck(message.author.id); 43 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; //this command can use admin only 44 | let obj = ReadDBFile(); 45 | 46 | if (args.length >= 2) { 47 | let [newCardName, classNumber, imgSrc = undefined] = args; 48 | // stage 1 check cardname if exsists 49 | newCardName = newCardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); 50 | console.log(newCardName); // "SPACE_SYMBOL" should use as ' ' if you want to add space in cardname 51 | if (FindCardByName(message, newCardName, true) != 0) { 52 | message.reply(`${LOCALES.AddNewCard__MessageEmbed__name_already_exists[CONSTANTS.LANG]}`); 53 | return; 54 | } 55 | // stage 2 check classNumber is it int number? 56 | if (!typeof(parseInt(classNumber, 10)) == 'number') { 57 | message.reply(`${LOCALES.AddNewCard__MessageEmbed__class_number[CONSTANTS.LANG]}`); 58 | return; 59 | } 60 | // stage 3 check imgSrc 61 | if (!imgSrc) { 62 | if (message.attachments.size > 0) { 63 | imgSrc = message.attachments.first().url 64 | } else { 65 | message.reply(`${LOCALES.AddNewCard__MessageEmbed__media_not_found[CONSTANTS.LANG]}`); 66 | return; 67 | } 68 | } 69 | if (!attachIsImage({url: imgSrc})) { 70 | message.reply(`${LOCALES.AddNewCard__MessageEmbed__media_incorrect[CONSTANTS.LANG]}`); 71 | return; 72 | } 73 | let newCard = 74 | { 75 | "name": newCardName, 76 | "class": classNumber, 77 | "active": true, 78 | "url": imgSrc 79 | } 80 | obj.cards.push(newCard) 81 | 82 | SaveObjToDB(obj); 83 | showNewCard(message, newCard, obj, client); 84 | if (isUrlMp4(imgSrc)) { 85 | message.reply(imgSrc); 86 | } 87 | } 88 | } 89 | 90 | module.exports = { 91 | name: `${LOCALES.AddNewCard__EXPORTS__name[CONSTANTS.LANG]}`, 92 | usage() { return `${CONSTANTS.PREFIX}${this.name} CardName ClassNumber [ImageSourceLink]` }, 93 | desc: `${LOCALES.AddNewCard__EXPORTS__desc[CONSTANTS.LANG]}`, 94 | func: AddNewCard, 95 | permission: 'ADMINISTRATOR' 96 | }; -------------------------------------------------------------------------------- /commands/EditCard.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const FindCardByName = require('../utils/FindCardByName.js'); 5 | const GetClassString = require("../utils/GetClassString.js"); 6 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 7 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 8 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 9 | const LOCALES = require('../constants/locales.js'); 10 | 11 | function attachIsImage(msgAttach) { 12 | var url = msgAttach.url; 13 | //True if this url is a png/jpg/gif image. 14 | return url.indexOf("png", url.length - 3) !== -1 || url.indexOf("jpg", url.length - 3) !== -1 || url.indexOf("gif", url.length - 3) !== -1 || url.indexOf("mp4", url.length - 3); 15 | } 16 | 17 | function isUrlMp4(url) { 18 | return url.indexOf("mp4", url.length - 3) !== -1; 19 | } 20 | 21 | function showNewCard(message, card, obj, client) { 22 | let cardClassNumber = obj.cards.find(cardDB => {return cardDB.name == card.name}).class; 23 | let cardClassString = GetClassString(cardClassNumber); 24 | client.users.fetch(message.author.id).then(user => { 25 | let embed = new EmbedBuilder(); 26 | embed.setColor("#d1b91f"); 27 | embed.setAuthor({name: user.username, iconURL: user.displayAvatarURL(), url: user.url}); 28 | embed.setTitle(`${LOCALES.EditCard__MessageEmbed__edited_card_with_name[CONSTANTS.LANG]}`); 29 | embed.setDescription(`**${(cardClassString) ? cardClassString : ReplaceEmojisFromNameToClass(card)} [${card.name}](${card.url})**`); 30 | embed.setImage(`${card.url}`); 31 | message.reply({embeds: [embed]}); 32 | }); 33 | } 34 | 35 | function EditCard (message, args, client) { 36 | UserCheck(message.author.id); 37 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; //this command can use admin only 38 | let obj = ReadDBFile(); 39 | //TODO edit card by 1 argument (cardname) 40 | if (args.length >= 3) { //strong edit 41 | let [CardName, editCardName, editClassNumber, imgSrc = undefined] = args; 42 | CardName = CardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); // "SPACE_SYMBOL" should use as ' ' if you want to add space in cardname 43 | const OldCard = FindCardByName(message, CardName, true); 44 | const EditCard = obj.cards.find(card => { if(card.name == OldCard.name) return card});// get reference of object to edit and save to DB after all 45 | // stage 1 check cardname if exsists 46 | editCardName = editCardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); // ";" should use as ' ' if you want to add space in cardname 47 | 48 | // stage 2 check editClassNumber is it int number? 49 | if (!typeof(parseInt(editClassNumber, 10)) == 'number') { 50 | message.reply(`${LOCALES.EditCard__MessageEmbed__class_number[CONSTANTS.LANG]}`); 51 | return; 52 | } 53 | // stage 3 check imgSrc 54 | if (!imgSrc) { 55 | if (message.attachments.size > 0) { 56 | imgSrc = message.attachments.first().url 57 | } else { 58 | message.reply(`${LOCALES.EditCard__MessageEmbed__media_not_found[CONSTANTS.LANG]}`); 59 | return; 60 | } 61 | } 62 | if (!attachIsImage({url: imgSrc})) { 63 | message.reply(`${LOCALES.EditCard__MessageEmbed__media_incorrect[CONSTANTS.LANG]}`); 64 | return; 65 | } 66 | // edit card in card pool 67 | EditCard.name = editCardName; 68 | EditCard.class = editClassNumber; 69 | EditCard.url = imgSrc; 70 | for (let usr of obj.users) { //apply edit to all users 71 | let finded = false; 72 | for(let usrcard of usr.cards) { 73 | if (!finded && (usrcard.name == OldCard.name)) { 74 | usrcard.name = EditCard.name; 75 | usrcard.url = EditCard.url; 76 | finded = true; 77 | } 78 | } 79 | } 80 | SaveObjToDB(obj); 81 | showNewCard(message, EditCard, obj, client); 82 | if (isUrlMp4(EditCard.url)) { 83 | message.reply(EditCard.url); 84 | } 85 | } 86 | 87 | if (args.length != 0) { 88 | let obj = ReadDBFile(); 89 | let CardName = args[0]; 90 | if (CardName.match(/-i|-n|-c|-a/)) { 91 | 92 | } 93 | 94 | CardName = CardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); // "SPACE_SYMBOL" should use as ' ' if you want to add space in cardname 95 | const OldCard = FindCardByName(message, CardName, true); 96 | const EditCard = obj.cards.find(card => { if(card.name == OldCard.name) return card});// get reference of object to edit and save to DB after all 97 | 98 | let editCardName = args.find(elem => {return `${elem}`.startsWith('-n')}); 99 | let imageURL = args.find(elem => {return `${elem}`.startsWith('-i')}); 100 | let cardClass = args.find(elem => {return `${elem}`.startsWith('-c')}); 101 | let active = args.find(elem => {return `${elem}`.startsWith('-a')}); 102 | 103 | 104 | // Check editCardName 105 | if (editCardName) { 106 | editCardName = editCardName.replace("-n", ''); 107 | editCardName = editCardName.replaceAll(CONSTANTS.SPACE_REGEX, ' '); 108 | EditCard.name = editCardName; 109 | } 110 | 111 | // Check editCardName 112 | if (imageURL) { 113 | imageURL = imageURL.replace("-i", ''); 114 | if (imageURL.matchAll('/^\d{2}[./-]\d{2}[./-]\d{4}$/')) { 115 | defaultCode.imageURL = new Date(imageURL); 116 | } 117 | } 118 | 119 | if (cardClass) { 120 | cardClass = cardClass.replace("-c", ''); 121 | EditCard.cardClass = cardClass; 122 | } 123 | 124 | if (active) { 125 | active = active.replace("-a", ''); 126 | active = (active == "false" ? false : true); //boolean, true by default 127 | EditCard.active = active; 128 | } 129 | } 130 | } 131 | 132 | module.exports = { 133 | name: `${LOCALES.EditCard__EXPORTS__name[CONSTANTS.LANG]}`, 134 | usage() { return `${CONSTANTS.PREFIX}${this.name} CardName, editCardName editClassNumber [editImageSourceLink]` }, 135 | desc: `${LOCALES.EditCard__EXPORTS__desc[CONSTANTS.LANG]}`, 136 | func: EditCard, 137 | permission: 'ADMINISTRATOR' 138 | }; -------------------------------------------------------------------------------- /commands/GiveCard.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const FindCardByName = require("../utils/FindCardByName.js"); 5 | const GetClassString = require("../utils/GetClassString.js"); 6 | const GetUserFromMention = require("../utils/GetUserFromMention.js"); 7 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 8 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 9 | const {EmbedBuilder, PermissionsBitField} = require('discord.js'); 10 | const LOCALES = require("../constants/locales.js"); 11 | 12 | /** 13 | * Optimized: Checks if a URL ends with ".mp4" (case-insensitive for safety). 14 | * @param {string} url The card URL. 15 | * @returns {boolean} True if the URL ends with .mp4. 16 | */ 17 | function isUrlMp4(url) { 18 | if (!url || typeof url !== 'string') return false; 19 | // Use .toLowerCase() and endsWith for robustness 20 | return url.toLowerCase().endsWith(".mp4"); 21 | } 22 | 23 | /** 24 | * Constructs and sends the confirmation message after a card is given. 25 | * Uses async/await for cleaner user fetching. 26 | * @param {Object} message The Discord message object. 27 | * @param {Object} card The master card object (with name, url). 28 | * @param {Object} obj The full database object. 29 | * @param {Object} client The Discord client object. 30 | * @param {string} memberId The ID of the recipient user. 31 | * @param {number} addCardCount The number of cards given. 32 | */ 33 | async function showGivenCard(message, card, obj, client, memberId, addCardCount = 1) { 34 | // Optimized: Find card class once 35 | const masterCard = obj.cards.find(cardDB => cardDB.name == card.name); 36 | const cardClassNumber = masterCard ? masterCard.class : 0; 37 | const cardClassString = GetClassString(cardClassNumber); 38 | 39 | let memberName; 40 | let authorUser; 41 | 42 | try { 43 | // Fetch users concurrently 44 | [memberName, authorUser] = await Promise.all([ 45 | client.users.fetch(memberId).then(user => user.username), 46 | client.users.fetch(message.author.id) 47 | ]); 48 | } catch (error) { 49 | console.error("Error fetching users for showGivenCard:", error); 50 | memberName = memberId; // Fallback 51 | authorUser = message.author; // Fallback 52 | } 53 | 54 | const embed = new EmbedBuilder() 55 | .setColor(0xd1b91f) 56 | .setAuthor({ 57 | name: authorUser.username, 58 | iconURL: authorUser.displayAvatarURL() 59 | }) 60 | .setTitle(`${LOCALES.GiveCard__MessageEmbed__issued_a_card[CONSTANTS.LANG]} ${memberName}:`) 61 | .setDescription(`**${(cardClassString) ? cardClassString : ReplaceEmojisFromNameToClass(card)} [${card.name}](${card.url})**`) 62 | .setFooter({text: `${LOCALES.DropCard__MessageEmbed__cards_you_have_now[CONSTANTS.LANG]} ${addCardCount}`}); // в количестве 63 | 64 | // Handle video/image display 65 | if (isUrlMp4(card.url)) { 66 | // Embed image is null for videos; the video URL is sent separately 67 | embed.addFields({ 68 | name: "🎥 Video Card", 69 | value: `${LOCALES.ShowCards__VideoCardViewerBelow[CONSTANTS.LANG]}`, 70 | inline: false 71 | }); 72 | } else { 73 | // Standard image/GIF 74 | embed.setImage(card.url); 75 | } 76 | 77 | // Send the main embed 78 | await message.reply({ embeds: [embed] }); 79 | 80 | // Send the video link separately if it's an MP4 81 | if (isUrlMp4(card.url)) { 82 | await message.channel.send(card.url); 83 | } 84 | } 85 | 86 | /** 87 | * Main command function for giving a card. Renamed from RoleTeter to GiveCardCommand. 88 | */ 89 | function GiveCardCommand(message, args, client) { 90 | UserCheck(message.author.id); 91 | 92 | if (!message.guild) return; 93 | // Check for explicit Administrator permission 94 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 95 | return; 96 | } 97 | 98 | let memberId; 99 | // 1. Get Member ID 100 | if (args[0]) { 101 | const member = GetUserFromMention(args[0]); 102 | if (!member) { 103 | message.reply(`${LOCALES.GiveCard__MessageEmbed__wrong_user[CONSTANTS.LANG]}`); 104 | return; 105 | } 106 | memberId = member.id; 107 | } else { 108 | // Must specify a user to give a card to 109 | message.reply("You must specify a user to give the card to."); 110 | return; 111 | } 112 | 113 | UserCheck(memberId); 114 | 115 | // 2. Get Card 116 | if (!args[1]) { 117 | message.reply("You must specify a card name."); 118 | return; 119 | } 120 | 121 | const card = FindCardByName(message, args[1]); 122 | if (!card) { 123 | // FindCardByName should handle the error message if card is not found 124 | return; 125 | } 126 | 127 | // 3. Get Count 128 | let addCardCount = 1; 129 | if (args[2]) { 130 | const customAddNumber = parseInt(args[2], 10); 131 | if (isNaN(customAddNumber) || customAddNumber < 1) { 132 | message.reply("The card count must be a positive number."); 133 | return; 134 | } 135 | addCardCount = customAddNumber; 136 | } 137 | 138 | // 4. Update Database 139 | const obj = ReadDBFile(); 140 | const user = obj.users.find(usr => usr.id == memberId); 141 | 142 | // Safety check (UserCheck should have created the user, but ensures safety) 143 | if (!user) { 144 | message.reply("An internal error occurred: User not found in database after check."); 145 | return; 146 | } 147 | 148 | const userCard = user.cards.find(item => item.name == card.name); 149 | 150 | if (userCard) { 151 | // Card exists: Update count and URL 152 | userCard.count += addCardCount; 153 | userCard.url = card.url; // Ensure URL is always updated from master card 154 | } else { 155 | // Card does not exist: Add new card 156 | user.cards.push({ 157 | "name": card.name, 158 | "count": addCardCount, 159 | "url": card.url 160 | }); 161 | } 162 | 163 | SaveObjToDB(obj); 164 | 165 | // 5. Show Confirmation 166 | // showGivenCard is called without await, allowing the command to finish faster, 167 | // but the function itself handles the asynchronous message sending. 168 | showGivenCard(message, card, obj, client, memberId, addCardCount); 169 | } 170 | 171 | module.exports = { 172 | name: `${LOCALES.GiveCard__EXPORTS__name[CONSTANTS.LANG]}`, // выдайкарту 173 | usage() { return `${CONSTANTS.PREFIX}${this.name} @UserMention CardName [count]`; }, 174 | desc: `${LOCALES.GiveCard__EXPORTS__desc[CONSTANTS.LANG]}`, 175 | func: GiveCardCommand, // Renamed function 176 | permission: 'ADMINISTRATOR' 177 | }; -------------------------------------------------------------------------------- /commands/Profile.js: -------------------------------------------------------------------------------- 1 | // You might need to adjust the paths to your utility files and constants 2 | const UserCheck = require("../utils/UserCheck.js"); 3 | const ReadDBFile = require("../utils/ReadDBFile.js"); 4 | const { EmbedBuilder } = require('discord.js'); 5 | const CONSTANTS = require ("../constants/constants.js"); 6 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 7 | const GetClassString = require("../utils/GetClassString.js"); 8 | // Removed GetUserFromMention as message.mentions is preferred in modern DJS 9 | const LOCALES = require("../constants/locales.js"); 10 | 11 | // --- Utility Functions (Keep them or move them) --- 12 | 13 | /** 14 | * Reads the DB and finds a user's cards based on userId. 15 | * Note: This function re-reads the DB every call, which might be inefficient. 16 | */ 17 | function GetUserCards(userId) { 18 | const obj = ReadDBFile(); 19 | // Use find for efficiency, then optional chaining for safety 20 | return obj.users.find(user => userId == user.id)?.cards || []; 21 | } 22 | 23 | function isUrlMp4(url) { 24 | if (!url) return false; 25 | // Checks if the URL ends with '.mp4' 26 | return url.toLowerCase().endsWith(".mp4"); 27 | } 28 | 29 | // --- Main Command Function (using async/await) --- 30 | 31 | const ShowProfile = async (message, args, client) => { 32 | // 1. Determine target user ID 33 | let targetUser = message.author; 34 | let memberId; 35 | 36 | // Check if a mention was provided in args[0] 37 | if (args[0]) { 38 | // Use mentions collection for a more reliable check 39 | const mentionedUser = message.mentions.users.first(); 40 | if (mentionedUser) { 41 | targetUser = mentionedUser; 42 | } else { 43 | // Handle error if argument is present but not a valid mention 44 | // Note: Your old logic assumed GetUserFromMention handled ID lookup or error 45 | // For simplicity with mentions, we stick to the error message. 46 | message.channel.send(`${LOCALES.Profile__MessageEmbed__wrong_user[CONSTANTS.LANG]}`); 47 | return; 48 | } 49 | } 50 | 51 | memberId = targetUser.id; 52 | 53 | // 2. Perform initial checks and read data 54 | UserCheck(message.author.id); // Check the message author 55 | UserCheck(memberId); // Check the target member 56 | 57 | const dbObj = ReadDBFile(); 58 | const userCards = GetUserCards(memberId); 59 | 60 | // 3. Fetch User (needed for avatar/username, though we have targetUser) 61 | // Using await for cleaner flow 62 | let user; 63 | try { 64 | // Fetch the user object if it's not already cached (e.g., if targetUser was only an ID) 65 | // Since we are using targetUser = message.mentions.users.first() or message.author, this is redundant but kept for robustness. 66 | user = await client.users.fetch(memberId); 67 | } catch (e) { 68 | console.error("Error fetching user:", e); 69 | message.channel.send(`${LOCALES.Profile__MessageEmbed__wrong_user[CONSTANTS.LANG]}`); 70 | return; 71 | } 72 | 73 | 74 | // 4. Handle Case: No Cards 75 | if (userCards.length == 0) { 76 | message.reply({ 77 | content: `**${user.username} ${LOCALES.Profile__MessageEmbed__no_cards_in_the_inventory[CONSTANTS.LANG]}**` 78 | }); 79 | return; 80 | } 81 | 82 | // 5. Build Embed 83 | const embed = new EmbedBuilder() 84 | .setThumbnail(user.displayAvatarURL()) 85 | .setTitle(`${LOCALES.Profile__MessageEmbed__user_profile[CONSTANTS.LANG]} ${user.username}`) 86 | .setColor(0x0099ff); // Optional: Set a color 87 | 88 | // Total Card Count 89 | const totalCardCount = userCards.reduce((sum, current) => sum + current.count, 0); 90 | embed.addFields({ 91 | name: `**${LOCALES.Profile__MessageEmbed__cards_fallen_total[CONSTANTS.LANG]} ${totalCardCount}**`, 92 | value: `** **` 93 | }); 94 | 95 | // Statistics Header 96 | embed.addFields({ 97 | name: `**${LOCALES.Profile__MessageEmbed__statistics_of_dropped_cards[CONSTANTS.LANG]}**`, 98 | value: `** **` 99 | }); 100 | 101 | // Card Class Statistics Loop 102 | for (let cardClass = 1; cardClass <= CONSTANTS.RARE_CLASS_NUMBER; cardClass++) { 103 | // Total cards in this class in DB 104 | const totalClassCount = dbObj.cards.filter(card => card.class == cardClass).length; 105 | 106 | // Count of cards from this class collected by the user 107 | const classCount = userCards.filter(userCard => { 108 | const dbCard = dbObj.cards.find(cardDB => cardDB.name == userCard.name); 109 | return dbCard && dbCard.class == cardClass; 110 | }).length; 111 | 112 | embed.addFields({ 113 | name: `${GetClassString(cardClass)}: ${classCount} ${LOCALES.Profile__MessageEmbed__of[CONSTANTS.LANG]} ${totalClassCount}`, 114 | value: `** **` 115 | }); 116 | } 117 | 118 | // Non-Standard Cards 119 | const totalNonStandatClassCount = dbObj.cards.filter(card => card.class > CONSTANTS.RARE_CLASS_NUMBER || card.class <= 0).length; 120 | 121 | if (totalNonStandatClassCount) { 122 | const classNonStandatCount = userCards.filter(userCard => { 123 | const dbCard = dbObj.cards.find(cardDB => cardDB.name == userCard.name); 124 | const cClass = dbCard ? dbCard.class : 0; // Default to 0 if not found 125 | return (cClass > CONSTANTS.RARE_CLASS_NUMBER || cClass <= 0); 126 | }).length; 127 | 128 | embed.addFields({ 129 | name: `**${LOCALES.Profile__MessageEmbed__collected_non_standard_cards[CONSTANTS.LANG]} ${classNonStandatCount} ${LOCALES.Profile__MessageEmbed__of[CONSTANTS.LANG]} ${totalNonStandatClassCount}**`, 130 | value: `** **` 131 | }); 132 | } 133 | 134 | // Remaining Cards 135 | const remainingCards = dbObj.cards.length - userCards.length; 136 | embed.addFields({ 137 | name: `**${LOCALES.Profile__MessageEmbed__not_been_opened_yet[CONSTANTS.LANG]} ${remainingCards}**`, 138 | value: `** **` 139 | }); 140 | 141 | // Most Dropped Card (Highest count) 142 | // Sort array by count descending and take the first element 143 | const sortedCardArray = [...userCards].sort((a, b) => b.count - a.count); 144 | const mostDroppedCard = sortedCardArray[0]; // Guaranteed to exist because we checked userCards.length > 0 145 | 146 | // Find the class of the most dropped card 147 | const cardDbInfo = dbObj.cards.find(cardDB => cardDB.name == mostDroppedCard.name); 148 | const cardClass = cardDbInfo ? cardDbInfo.class : null; 149 | const cardClassString = cardClass ? GetClassString(cardClass) : ''; 150 | 151 | embed.addFields({ 152 | name: `**${LOCALES.Profile__MessageEmbed__fell_out_the_most_times[CONSTANTS.LANG]}**`, 153 | // Use a descriptive name if class string is empty 154 | value: `${cardClassString || ReplaceEmojisFromNameToClass(mostDroppedCard)} [${mostDroppedCard.name}](${mostDroppedCard.url}) X${mostDroppedCard.count}` 155 | }); 156 | 157 | // Set image and send reply 158 | embed.setImage(mostDroppedCard.url); 159 | 160 | // v14 uses message.reply({ embeds: [...] }) 161 | await message.reply({ embeds: [embed] }); 162 | 163 | // Send MP4 URL separately if needed, as DJS embeds struggle with MP4 previews 164 | if (isUrlMp4(mostDroppedCard.url)) { 165 | // Use message.channel.send to send the URL without a reply ping 166 | message.channel.send(mostDroppedCard.url); 167 | } 168 | } 169 | 170 | module.exports = { 171 | name: `${LOCALES.Profile__EXPORTS__name[CONSTANTS.LANG]}`, 172 | usage() { return `${CONSTANTS.PREFIX}${this.name} || ${CONSTANTS.PREFIX}${this.name} @UserMention `; }, 173 | desc: `${LOCALES.Profile__EXPORTS__desc[CONSTANTS.LANG]}`, 174 | func: ShowProfile, 175 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiscordCardBot 2 | A simple bot that allows you to collect cards by server participants. 3 | [Project Setup](#Project-Setup) [ru-README](./RU-README.md) 4 | ## Description 5 | Commands available to discord server participants: 6 | - [drop](#drop) : get a card 1 time in 24 hours or view after how long it can be received 7 | - [show](#show) : view the list and number of cards of the same type that user has already received 8 | - [profile](#profile) : view statistics of the server user on the drawn cards 9 | - [help](#help) : get help about bot commands 10 | - [undiscovered](#undiscovered) : find out the number of cards that have not fallen to any of the server participants 11 | - [giveacard](#giveacard) : issue 1 card to the specified user 12 | - [activate](#activate-Code) : activate the code for a chance to get the card again 13 | - [createcode](#createcode) : create a code that users can use to be able to get a card 14 | - [deletecard](#deletecard-CardName) : remove the card from the system and from all users 15 | - [addcard](#addcard) : add a new card to the card drop pool 16 | - [editcard](#editcard) : change an existing map in the system and for all users 17 | - [resetdrop](#resetdrop) : cooldown reset of drop for everyone or a specific user 18 | ### drop 19 | Drops a random card from the pool of cards existing in the system 1 time every 24 hours or 1 time a day at a certain time, depending on the bot settings. 20 | This command has a mechanic in which if the user receives the 3rd repeating card, the system gives a chance to use this command again. 21 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008477525267198014/unknown.png) 22 | ### show [@UserMention] 23 | When entering a command without arguments, it shows the inventory of the user - the author of the message. 24 | 25 | When adding a user mention as an argument (via @), the inventory of the specified user will be shown (provided that the admin has enabled this feature in the config: INVENTORY_PUBLIC_ACCESS = 1 ) 26 | 27 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008502106547818636/unknown.png) 28 | 29 | The inventory has a page interface, where the pages are changed by adding the reactions of arrows ( ⬅️ and ➡️ ) that the bot offers, the pages change only if the response under the message with the inventory was left by the author of the sent command. 30 | After some time (which is configured in the config: **INVENTORY_TIME**), when the inventory is inactive, there is no active page switching, the inventory stops working, and in order to view it again, you need to enter the necessary command again. 31 | 32 | ### profile [@UserMention] 33 | Shows the profile of the specified user or the author of the message with statistics of the collected cards for all time. 34 | 35 | ![](https://media.discordapp.net/attachments/852679774128439386/1008503033279299625/unknown.png) 36 | 37 | ### help 38 | Displays a list of all commands available to the author of the message. 39 | If the server administrator has requested help, the bot will display an expanded list of commands. 40 | 41 | ![](https://media.discordapp.net/attachments/852679774128439386/1008503228532543558/unknown.png) 42 | 43 | ### undiscovered 44 | Displays the number of cards that none of the registered participants have. 45 | 46 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008503947549495376/unknown.png) 47 | 48 | ### giveacard @UserMention CardName 49 | Gives the specified user a card found by the entered name (or part of the name). 50 | 51 | The command can be used **only by the server administrator**. To use this function, you need 2 required arguments: 52 | - @UserMention: the user to whom the card will be issued 53 | - CardName: the name of the card that will be issued in the amount of 1 to the user. This argument can take an incomplete name of the card, the system will find the most suitable one by the entered name of the card 54 | 55 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008505520358948985/unknown.png) 56 | 57 | ### activate Code 58 | Activation of the code created by the administrator, upon successful activation by the user, resets his cooldown for using the **drop** command 59 | 60 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008507041159057428/unknown.png) 61 | 62 | ### createcode [-c] [-u] [-d] 63 | Creates a special code that users can activate to be able to reset the cooldown of the **drop command** 64 | This command can only be used by the administrator, while 3 optional parameters are available to him for use, which can be used in any order to set restrictions on the use of the code: 65 | - -cCodeName : custom code name 66 | - -uUsingCount : the number of unique users who can use the code 67 | - -dmm/dd/yyyy : the expiration date of the code, after which users will not be able to use it 68 | 69 | ![](https://media.discordapp.net/attachments/852679774128439386/1008506789672779826/unknown.png) 70 | 71 | By default, an automatically generated code is created with no expiration date for 1 use. 72 | 73 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008507346982547466/unknown.png) 74 | 75 | ### deletecard CardName 76 | The admin command that removes the specified card from the system and from all users. 77 | 78 | This command requires 1 mandatory argument - **the full name of the card.** 79 | 80 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008512987608403998/unknown.png) 81 | 82 | **If there is a space in the name of the card, then use ";" instead, because the space is used to separate arguments.** 83 | 84 | ### addcard CardName ClassNumber [ImageSourceLink] 85 | The server admin can create a new card that is automatically placed in the card drop pool. 86 | Command arguments: 87 | - CardNameCardName **name of the new card**, (required) - must not match the existing name of the card in the database, use the **SPACE_SYMBOL** symbol to add a space 88 | - ClassNumber **card class**, (required) it should be a number from 1 to 5 - standard classes of cards, other values will make a card of a non-standard class 89 | - ImageSourceLink **link to the picture .png .jpg .gif**, a mandatory argument that can be omitted provided that a picture is attached to the message of this command in the attachment. 90 | 91 | ![](https://media.discordapp.net/attachments/852679774128439386/1008514371703541780/unknown.png) 92 | 93 | ### editcard CardName editCardName editClassNumber [editImageSourceLink] 94 | A command accessible only to the administrator, created to change the data of existing maps, requires the following arguments: 95 | - CardName (required) - full name of the card to be changed 96 | - editCardName (required) - the new name of the card, if you do not want to change, insert the one that is (a reminder to use **SPACE_SYMBOL** instead of a space) 97 | - editClassNumber - (required) new card class, must be a number 98 | - editImageSourceLink (required) - new image (link to image) to display 99 | 100 | ![](https://media.discordapp.net/attachments/852679774128439386/1008515485215764480/unknown.png) 101 | 102 | ### resetdrop [@UserMention] 103 | Resets the cooldown of the **drop** command for all users or only the specified one. (Available only to the administrator) 104 | 105 | ![](https://media.discordapp.net/attachments/852679774128439386/1008515882055651358/unknown.png) 106 | 107 | ## News 108 | **a small update from 09.25.2022** 109 | - Added option for replacing ' ' in commands: **SPACE_SYMBOL** , before it was ';' by default 110 | For example to add Card with name that contains ' ' in name you can use your own different symbol(s) (not allowed symbols that using as args separator): 111 | in .env file **SPACE_SYMBOL = "__"** 112 | ``` 113 | !addcard Hello__world 2 https://c.tenor.com/mGgWY8RkgYMAAAAC/hello-world.gif // cardname in DB: "Hello world" 114 | ``` 115 | 116 | **Major update from 08.11.2022** 117 | - Added administration of cards using commands (add, change and delete) 118 | - Added the ability to select a language and add your own localization, now the bot supports Russian and English. 119 | - Added the ability to create/activate special codes that users can use to get one more card 120 | - Added the function of reseting the cooldown of issuing cards to all users or to some specific 121 | - Fixed errors with user registration and caused the bot to stop working, added new ones) 122 | - Added display of the number of non-standard cards in the user profile 123 | - When receiving a card, it is now indicated how many of the same the user has in the inventory 124 | 125 | 126 | **feature from 09.12.2021** 127 | - now you can set the server time at which the timer of all users to receive the card (**drop**) is reset. 128 | 129 | **update from 11/30/2021** 130 | - Links are embedded in the name of the cards, which allows you to use urls of any length without spoiling the type of message. 131 | - Fixed a bug where the value (**class**) of cards with symbols/emojis in **show me** was not displayed. 132 | - Removed the url display in **profile** (now the link is in the name of the map). 133 | - Changed the appearance for **drop**. 134 | 135 | ![](https://media.discordapp.net/attachments/852679774128439386/915276541347381268/drop.png) 136 | 137 | - Changed the appearance for **show**. 138 | 139 | ![](https://media.discordapp.net/attachments/852679774128439386/915276541573881896/покажимне.png) 140 | 141 | - Added the ability to view someone else's inventory (**show me @UserMention**), provided that this function is allowed (by the parameter in the config **INVENTORY_PUBLIC_ACCESS = 1**). 142 | 143 | **a small update from 11.21.2021** 144 | - Added test cards to **db.json** for an example of adding them. 145 | 146 | ![](https://cdn.discordapp.com/attachments/852679774128439386/911969469914574939/unknown.png) 147 | 148 | - Added display of the number of cards of each class when viewing the profile (**profile**). 149 | 150 | ![](https://cdn.discordapp.com/attachments/852679774128439386/911967665906651166/unknown.png) 151 | 152 | **feature from 06.10.2021** 153 | - Added the **undiscovered** command. Displays the number of cards that no one has been able to get on the server yet. 154 | 155 | ![](https://cdn.discordapp.com/attachments/852679774128439386/895117207615471666/unknown.png) 156 | 157 | **feature from 09.26.2021** 158 | - added the **profile** [@UserMention] command. Displays a user profile containing information: 159 | - how many cards have fallen out in total; 160 | - how many cards of a certain class have fallen out; 161 | - the number of cards that the user has not opened yet; 162 | - the card that fell out the most times. 163 | - added fields for cards: **class** (card class), designed to determine the value /rarity of the card. 164 | 165 | **a small update from 08.04.2021** 166 | - When viewing the inventory, not only the current page is now shown, but also how many pages are available in total. 167 | - When the inventory expires, all reactions attached to the message are deleted. 168 | 169 | **feature from 31.07.2021** 170 | - In order not to exceed the maximum number of characters in the message, a "page" inventory was introduced for the Discord API: 171 | after entering the command about the show inventory request, the bot shows you the last inventory page, which contains your recently received collectible cards. You can flip through the inventory pages using the reactions ( ⬅️ and ➡️ ) in the message with your cards. Keep in mind, the bot will not wait indefinitely for you to scroll through the page, after a while it will stop responding to new reactions, and you will have to request inventory with the command again. 172 | 173 | **feature from 11.06.2021** 174 | - When receiving the same card every 3rd time, the server participant is given the opportunity to immediately try to knock out another card. 175 | - Now, when a person uses the command to receive a card, 24 hours have not passed since the last receipt, the bot will show how long the card will be available. 176 | 177 | ## Project Setup 178 | 1. **Clone the project.** 179 | 180 | 2. **Make sure that you have installed:** 181 | - ![Node.js](https://nodejs.org/en/) 10.0.0 or higher. 182 | 183 | 3. **Create a configuration file and enter the necessary information into it.** 184 | 185 | ### Creating a config 186 | In the root of the project, create a configuration file **.env** 187 | Example of the .env file : 188 | ``` 189 | TOKEN = "your bot token" 190 | PREFIX = '!' 191 | PAGE_SIZE = 7 192 | INVENTORY_TIME = 60000 193 | INVENTORY_PUBLIC_ACCESS = 1 194 | RARE_CLASS_NUMBER = 5 195 | CLASS_SYMBOL_FILL = ":star:" 196 | CLASS_SYMBOL_OF_VOID = ":small_orange_diamond:" 197 | RESET_LOCAL_TIME = "" 198 | SPACE_SYMBOL = "__" 199 | LOCALES = "en" 200 | ``` 201 | 202 | **PAGE_SIZE** - the number of items that will fit on one inventory page when displayed 203 | 204 | **INVENTORY_TIME** - the idle time of the inventory, after which the bot stops flipping pages, is indicated in milliseconds 205 | 206 | **INVENTORY_PUBLIC_ACCESS** - access to another user's inventory **1 - you can use show @UserMention, 0 - other users can only show their inventory themselves in the chat** 207 | 208 | **RESET_LOCAL_TIME** - the local time of the server during which the timer is reset for **drop** to all users at the same time. Field value format: **"hh:mm:ss"**, for example **RESET_LOCAL_TIME** = "16:45:00". If **""** - then when using **drop**, the bot will set the reset time for each user individually. 209 | 210 | **LOCALES** - setting up the localization of the bot en - English, ru - Russian 211 | 212 | **SPACE_SYMBOL** - Symbol(s) should use instead of SPACE in commands, because ' ' by default is using for splitting arguments 213 | 214 | **RARE_CLASS_NUMBER** - number of rarity/value classes 215 | 216 | **CLASS_SYMBOL_FILL** - Discord emoji to fill in the rarity/value scale 217 | 218 | **CLASS_SYMBOL_OF_VOID** - Discord emoji to fill the void of the rarity/value scale 219 | 220 | 221 | 222 | The illustration below is to understand the essence of the last 3 parameters: 223 | 224 | ![](https://media.discordapp.net/attachments/852679774128439386/891748889118511134/env_decr.png) 225 | 226 | 4. **Add the necessary content for your server.** 227 | 228 | ### Content preparation 229 | Open /storage/db.json 230 | ``` 231 | { 232 | "users": [], // Contains data of server participants and their inventory 233 | "cards": [], // Contains data about cards that may fall out on the server 234 | "codes": [], // Contains information about event codes for users 235 | } 236 | ``` 237 | 238 | Data about server users ** will be automatically added** as the bot commands are used. 239 | You ** can fill in the card data by yourself**. 240 | Example of adding a map to **db.json** 241 | ``` 242 | cards: [ 243 | { 244 | "name": "test_card", // Name of the card 245 | "class": 1, // the value of the card is determined from 1 to RARE_CLASS_NUMBER inclusive 246 | "active": true, // Flag that determines whether the card can fall to the server participants 247 | "url": "url_string.png" // Link to the card image 248 | }, 249 | ] 250 | ``` 251 | 252 | If you have only cloned the project, then you may find that in **db.json** already has information about test cards with a rarity from 1 to 6 where cards with **class from 1 to 5 are standard** and the one with **class 6 is non-standard** (Which means its value will not be displayed using **CLASS_SYMBOL_FILL** and **CLASS_SYMBOL_OF_VOID**, keep this in mind, if you want to somehow mark its value with symbols/emojis, you can write them in the card name field, as in **db.json** of the repository). 253 | 254 | 5. **Download the necessary modules for the project to work.** 255 | 256 | ### Download dependencies 257 | Open the terminal in the root of the project, then write the following commands: 258 | ``` 259 | npm i 260 | ``` 261 | 6. **After all the steps have been successfully completed, you can start the bot by writing a command in the terminal.** 262 | ### Project launch 263 | ``` 264 | npm start 265 | ``` -------------------------------------------------------------------------------- /RU-README.md: -------------------------------------------------------------------------------- 1 | # DiscordCardBot 2 | Простенький бот, позволяющий коллекционировать карты участниками сервера. 3 | [Настройка проекта](#Настройка-проекта) [en-README](./README.md) 4 | ## Описание 5 | Участник дискорд сервера может при использовании команд этого бота: 6 | - [дайкарту](#дайкарту) : получить коллекционную карту 1 раз в 24 часа или просмотреть через сколько времени он ее сможет получить 7 | - [покажимне](#покажимне) :просмотреть список и количество карт одного типа, которые он уже получил 8 | - [профиль](#профиль) : просмотреть статистику пользователя сервера по выпавшим картам 9 | - [помощь](#помощь) : получить справку о командах бота 10 | - [неисследовано](#неисследовано) : узнать количество карт, которых не выпало ни одному из участников сервера на данный момент 11 | - [выдайкарту](#выдайкарту) : выдать 1 карту указанному пользователю 12 | - [активируй](#активируй-Code) : активировать код для получения шанса получить катру еще раз 13 | - [создатькод](#создатькод) : создать код, который могут использовать пользователи для получения возможности получить карту 14 | - [удалитькарту](#удалитькарту-CardName) : удалить карту из системы и у всех пользователей 15 | - [новаякарта](#новаякарта) : добавить в пул выпадения карт новую карту 16 | - [изменитькарту](#изменитькарту) : изменить существующую карту в системе и у всех пользователей 17 | - [обновикрутки](#обновикрутки) : дать возможность всем или конкретному пользователю получить карту 18 | ### дайкарту 19 | Выдает рандомную карту из пула существующих в системе карточек 1 раз в 24 часа или же 1 раз в день в определенное время в зависимости от настройки бота. 20 | У этой команды присутствует механика, при которой если пользователь получает 3ю повторяющуюся карту, то система дает шанс использовать эту команду еще раз. 21 | ![](https://media.discordapp.net/attachments/852679774128439386/1008089528965267487/unknown.png) 22 | ### покажимне [@UserMention] 23 | При вводе команды **без аргументов** показывает инвентарь пользователя - **автора сообщения**. 24 | 25 | При добавлении в качестве аргумента **упоминания пользователя(через @)** покажет инвентарь указанного пользователя (при условии что админ включил данную возможность в конфиге: **INVENTORY_PUBLIC_ACCESS = 1** ) 26 | 27 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008397459397017600/unknown.png) 28 | 29 | Инвентарь имеет страничный интерфейс, где страницы меняются путем добавлений реакций стрелочек(⬅️ и ➡️) которые предлагает бот, страницы меняются только в том случае если реацкцию под сообщением с инвентарем оставил автор отправленной команды. 30 | Спустя некоторое время (которое настраивается в конфиге: **INVENTORY_TIME**) при бездействии инвентаря - отсутсвтие активного переключения страниц, инвентарь перестает работать, и для того чтобы еще раз его посмотреть нужно снова ввести нужную команду. 31 | 32 | ### профиль [@UserMention] 33 | Показывает профиль указанного пользователя или автора сообщения со статистикой собранных карт за все время. 34 | 35 | ![](https://media.discordapp.net/attachments/852679774128439386/1008119707318091816/unknown.png) 36 | 37 | ### помощь 38 | Выводит перечень всех команд, доступных автору сообщения. 39 | Если помощь запросил администратор сервера, то бот выведет расширенный список команд. 40 | 41 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008404455957483602/unknown.png) 42 | 43 | ### неисследовано 44 | Выводит количество карт, которых нет ни у одного из зарегистрированных участников. 45 | 46 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008407426413891654/unknown.png) 47 | 48 | ### выдайкарту @UserMention CardName 49 | Выдает указанному пользователю карту, найденную по вписанному имени(или части имени). 50 | 51 | Команду может использовать **только администратор сервера**. Для использования этой функции нужны 2 обязательных аргумента: 52 | - @UserMention: пользователь которому будет выдаваться карта 53 | - CardName: название карты, которая будет выдана в количестве 1 штуки пользователю. Этот аргумент может принимать неполное название карты, система найдет наиболее подоходящую по введеному имени карты 54 | 55 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008408519663431681/unknown.png) 56 | 57 | ### активируй Code 58 | Активация кода, созданного администратором, при успешной активации пользователем обнуляет ему кулдаун для использования команды **дайкарту** 59 | 60 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008413555302879302/unknown.png) 61 | 62 | ### создатькод [-c] [-u] [-d] 63 | Создает специальный код который могут активировать пользователи для получения возможности сбросить кулдаун команды **дайкарту** 64 | Этой командой может воспользоваться только администратор при этом ему доступны для использования 3 необязательных параметра которые можно использовать в любом порядке для установки ограничений на использование кода: 65 | - -cCodeName : кастомное название кода 66 | - -uUsingCount : количество уникальных пользователей, которые могу использовать код 67 | - -dmm/dd/yyyy : дата истечения срока годности кода, после которой пользователи не смогут его использовать 68 | 69 | ![](https://media.discordapp.net/attachments/852679774128439386/1008413384590503957/unknown.png) 70 | 71 | По умолчанию создается автоматически сгенерированный код без срока годности на 1 использование. 72 | 73 | ![](https://media.discordapp.net/attachments/852679774128439386/1008418844068544582/unknown.png) 74 | 75 | ### удалитькарту CardName 76 | Админская команда, удаляющая из системы и у всех пользователей указанную карту. 77 | 78 | Данная команда требует 1 обязательный аргумент - **полное название карты.** 79 | 80 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008423799160582265/unknown.png) 81 | 82 | **Если в названии карты есть пробел, то вместо него используйте ";" тк пробел служит для разделения аргументов.** 83 | 84 | ### новаякарта CardName ClassNumber [ImageSourceLink] 85 | Админ сервера может создать новую карту, которая автоматически помещается в пул выпадения карт. 86 | Аргументы команды: 87 | - CardNameCardName **название новой карты**, (обязательный) - не должно совпадать с существующим именем карты в базе, для добавления пробела используйте **SPACE_SYMBOL** 88 | - ClassNumber **класс карты**, (обязательный) должно быть числом от 1 до 5 - стандартные классы карт, другие значения сделают картой нестандартного класса 89 | - ImageSourceLink **ссылка на картинку .png .jpg .gif**, обязательный аргумент, который можно не вводить при условии, что к сообщению этой команды во вложение прикреплена картинка. 90 | 91 | ![](https://media.discordapp.net/attachments/852679774128439386/1008427304545955962/unknown.png) 92 | 93 | ### изменитькарту CardName editCardName editClassNumber [editImageSourceLink] 94 | Команда доступная только администратору, созданная для изменения данных существующих карт, требует следующие аргументы: 95 | - CardName (обязательный) - полное название карты которую требуется изменить 96 | - editCardName (обязательный) - новое название карты, если не хотите менять вставьте то которое есть (напоминание использовать **SPACE_SYMBOL** вместо пробела) 97 | - editClassNumber - новый класс карты, должно быть числом 98 | - editImageSourceLink - новое изображение (ссылка на изображение) для отображения 99 | 100 | ![](https://media.discordapp.net/attachments/852679774128439386/1008434737418870914/unknown.png) 101 | 102 | ### обновикрутки [@UserMention] 103 | Сбрасывает кулдаун команды **дайкарту** для всех пользователей или только указанному. (Доступно только администратору) 104 | 105 | ![](https://cdn.discordapp.com/attachments/852679774128439386/1008436051947303073/unknown.png) 106 | 107 | ## Новости 108 | **небольшое обновление 09.25.2022** 109 | - Добавлена настройка для замены ' ' в командах: **SPACE_SYMBOL** , до этого использовалось ';' по умолчанию 110 | К примеру для добавления новой карты с именем содержащим ' ' вы можете использовать свой(и) символ(ы) (не предусмотрены символы которые и так служат для разделения аргументьов в коммандах): 111 | в .env файле **SPACE_SYMBOL = "__"** 112 | ``` 113 | !новаякарта Привет__мир 2 https://c.tenor.com/mGgWY8RkgYMAAAAC/hello-world.gif // Имя карты в БД: "Привет мир" 114 | ``` 115 | 116 | **Крупное обновление от 11.08.2022** 117 | - Добавлено администрирование карт при помощи команд(добавление, изменение и удаление) 118 | - Добавлена возможность выбора языка и добавления своей локализации, сейчас бот поддерживает русский и английский языки. 119 | - Добавлена возможность создания/активации специальных кодов которые могут использовать пользователи для получения круток 120 | - Добавлена функция обнуления кулдауна выдачи карт всем пользователям или какому-нибудь конкретному 121 | - Исправлены ошибки с регистрацией пользователей и влекших за собой остановку работы бота, добавлены новые) 122 | - Добавлено отображение количества нестандартных карт в профиле пользователя 123 | - При получении карты теперь указывается сколько таких же пользователь имеет в инвентаре 124 | 125 | 126 | **фича от 09.12.2021** 127 | - теперь можно установить серверное время в момент которого, таймер всех пользователей для получения карты(**дайкарту**) обнуляется. 128 | 129 | **обновление от 30.11.2021** 130 | - В названии карт встроены ссылки, что позволяет использовать url любой длинны без порчи вида сообщения. 131 | - Исправлен баг, при котором не отображалась ценность(**class**) карт символами/эмодзи в **покажимне**. 132 | - Убрано отображение url в **профиль** (теперь ссылка в названии карты). 133 | - Изменен внешний вид для **дайкарту**. 134 | 135 | ![](https://media.discordapp.net/attachments/852679774128439386/915276541347381268/drop.png) 136 | 137 | - Изменен внешний вид для **покажимне**. 138 | 139 | ![](https://media.discordapp.net/attachments/852679774128439386/915276541573881896/покажимне.png) 140 | 141 | - Добавлена возможность просматривать чужой инвентарь(**покажимне @UserMention**), при условии если данная функция разрешена(параметром в конфиге **INVENTORY_PUBLIC_ACCESS = 1**). 142 | 143 | **небольшое обновление от 21.11.2021** 144 | - Добавлены тестовые карты в **db.json** для примера их добавления. 145 | 146 | ![](https://cdn.discordapp.com/attachments/852679774128439386/911969469914574939/unknown.png) 147 | 148 | - Добавлено отображение количества карт каждого класса при просмотре профиля(**профиль**). 149 | 150 | ![](https://cdn.discordapp.com/attachments/852679774128439386/911967665906651166/unknown.png) 151 | 152 | **фича от 06.10.2021** 153 | - Добавлена команда **undiscovered**. Выводит количество карт, которых на сервере еще никому не удавалось получить. 154 | 155 | ![](https://cdn.discordapp.com/attachments/852679774128439386/895117207615471666/unknown.png) 156 | 157 | **фича от 26.09.2021** 158 | - Добавлена команда **профиль** [@UserMention]. Выводит профиль пользователя, содержащий информацию: 159 | - сколько карт всего выпало; 160 | - какое количество карт выпало определенного класса; 161 | - количество карт, которых пользователь еще не открыл; 162 | - карта, котороая больше всего раз выпала. 163 | - Добавлены поля для карт: **class**(класс карты), предназначенные для определения ценности/редкости карты. 164 | 165 | **небольшое обновление от 04.08.2021** 166 | - При просмотре инвентаря теперь показывается не только текущая страница, но и сколько всего страниц доступно. 167 | - При истечении срока работы инвентаря все реакции, прикрепленные к сообщению удаляются. 168 | 169 | **фича от 31.07.2021** 170 | - Чтобы не превышать максимальное количество символов в сообщении для API Discord`а был введен "страничный" инвентарь: 171 | после ввода команды о запросе показать инвентарь, бот показывает вам последнюю страницу инвентаря, в которой содержатся ваши недавно полученные коллекционные карты. Вы можете перелистывать страницы инвентаря при помощи реакций(⬅️ и ➡️) в сообщении с вашими картами. Учтите, бот не будет бесконечно ждать, когда вы соизволете пролистнуть страницу, через некоторое время он перестанет реагировать на новые реакции, и вам придется запросить инвентарь командой снова. 172 | 173 | **фича от 11.06.2021** 174 | - При получении одной и той же карты каждый 3й раз, участнику сервера дается возможность сразу же попытаться выбить еще одну карту. 175 | - Теперь, когда человек будет использовать команду для получения карты, но 24 часа с момента прошлого получения еще не прошли, бот покажет через сколько времни выдача карты будет доступна. 176 | 177 | ## Настройка проекта 178 | 1. **Склонируйте проект.** 179 | 180 | 2. **Убедитесь, что у вас установлено:** 181 | - ![Node.js](https://nodejs.org/en/) 10.0.0 или выше. 182 | 183 | 3. **Создайте конфигигурационный файл и занесите в него необходимую информацию.** 184 | 185 | ### Создание конфига 186 | В корне проекта создайте конфигурационный файл **.env** 187 | Пример файла .env : 188 | ``` 189 | TOKEN = "токен вашего бота" 190 | PREFIX = '!' 191 | PAGE_SIZE = 7 192 | INVENTORY_TIME = 60000 193 | INVENTORY_PUBLIC_ACCESS = 1 194 | RARE_CLASS_NUMBER = 5 195 | CLASS_SYMBOL_FILL = ":star:" 196 | CLASS_SYMBOL_OF_VOID = ":small_orange_diamond:" 197 | RESET_LOCAL_TIME = "" 198 | SPACE_SYMBOL = "__" 199 | LOCALES = "en" 200 | ``` 201 | 202 | **PAGE_SIZE** - количество элементов, которые будут умещаться на одной странице инвентаря при показе 203 | 204 | **INVENTORY_TIME** - время бездействия инвентаря, после которого бот перестает листать страницы, указывается в миллисекундах 205 | 206 | **INVENTORY_PUBLIC_ACCESS** - доступ вызова инвентаря другого пользователя **1 - можете использовать покажимне @UserMention, 0 - другие пользователи могут только сами демонстрировать свой инвентарь в чате** 207 | 208 | **RESET_LOCAL_TIME** - локальное время сервера во время которого происходит сброс таймера для **дайкарту** у всех пользователей одновременно. Формат значения поля: **"чч:мм:сс"**, например **RESET_LOCAL_TIME** = "16:45:00". Если **""** - то при использовании **дайкарту** бот будет устанавливать время сброса каждому пользователю индивидуально. 209 | 210 | **SPACE_SYMBOL** - Символ(ы) которые используются вместо пробела в командах, потому что ' ' по умолчанию используется для разделения аргументов 211 | 212 | **RARE_CLASS_NUMBER** - количество классов редкости/ценности 213 | 214 | **CLASS_SYMBOL_FILL** - Discord emoji для заполнения шкалы редкости/ценности 215 | 216 | **CLASS_SYMBOL_OF_VOID** - Discord emoji для заполнения пустоты шкалы редкости/ценности 217 | 218 | **LOCALES** - настройка локализации бота en - английский, ru - русский язык 219 | 220 | Иллюстрация ниже чтобы понять суть 3х последних параметров: 221 |
222 | ![](https://media.discordapp.net/attachments/852679774128439386/891748889118511134/env_decr.png) 223 | 224 | 4. **Добавьте необходимый контент для вашего сервера.** 225 | 226 | ### Подготовка контента 227 | Откройте файл /storage/db.json 228 | ``` 229 | { 230 | "users": [], // Ссодержит данные участников сервера и их инвентарь 231 | "cards": [], // Содержит данные о картах, которые могут выпасть на сервере 232 | "codes": [], // Содержит информацию о ивентовых кодах для пользователей 233 | } 234 | ``` 235 | 236 | Данные о пользователях сервера **будут автоматически добавляться** по мере использования команд бота. 237 | Данные о картах вам **придется заполнить самостоятельно**. 238 | Пример добавления карты в **db.json** 239 | ``` 240 | cards: [ 241 | { 242 | "name": "test_card", // Название карты 243 | "class": 1, // ценность карты, определяется от 1 до RARE_CLASS_NUMBER включительно 244 | "active": true, // Флаг, определяющий, может ли катра вывпасть участникам сервера 245 | "url": "url_string.png" // Ссылка на изображение карты 246 | }, 247 | ] 248 | ``` 249 | 250 | Если вы только склонировали проект, то вы можете обнаружить что в **db.json** уже есть информация о тестовых картах с редкостью от 1 до 6 где карты с **class от 1 до 5 - стандартные** и та, что с **class 6 - нестандартная**(Что значит ее ценность не будет отображаться при помощи **CLASS_SYMBOL_FILL** и **CLASS_SYMBOL_OF_VOID**, учтите это, если же вы хотите как то отметить ее ценность символами/эмодзи, можете прописать их в поле названия карты, как в **db.json** репозитория). 251 | 252 | 5. **Скачайте необходимые модули для работы проекта.** 253 | 254 | ### Подгрузка зависимостей 255 | Откройте терминал в корне проекта, далее пропишите следующие команды: 256 | ``` 257 | npm i 258 | ``` 259 | 6. **После всех успешно проделанных шагов, вы можете запустить бота, прописав команду в терминале.** 260 | ### Запуск проекта 261 | ``` 262 | npm start 263 | ``` -------------------------------------------------------------------------------- /commands/DropCard.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require("../constants/constants.js"); 4 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 5 | // NOTE: Removed AttachmentBuilder from imports as we are not uploading the file. 6 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require('discord.js'); 7 | const GetClassString = require("../utils/GetClassString.js"); 8 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 9 | const ReturnRequeredUTCDateToReset = require("../utils/TimeDiff.js"); 10 | const LOCALES = require("../constants/locales.js"); 11 | 12 | // Assume configLocalTime is configured in CONSTANTS or available globally. 13 | const configLocalTime = CONSTANTS.RESET_LOCAL_TIME; 14 | const COLLECTOR_TIMEOUT = 60000; // 60 seconds for re-roll button 15 | 16 | // --- Utility Functions --- 17 | 18 | function daysDiff(dt1, dt2) { 19 | dt2 = new Date(dt2); 20 | let diffTime = Math.abs(dt2.getTime() - dt1.getTime()); 21 | let daysDiff = diffTime / (1000 * 3600 * 24); 22 | return daysDiff; 23 | } 24 | 25 | /** 26 | * Gets a random card, optionally filtered by class. 27 | * @param {Array} allCards - All cards from the database. 28 | * @param {string|null} targetClass - The class ID to limit the drop pool to ('1', '2', ..., 'nonstandard'). 29 | * @returns {Object|null} The random card object. 30 | */ 31 | function GetRandomCard(allCards, targetClass = null) { 32 | let cardPool = allCards.filter(card => card.active); 33 | 34 | // Filter by class if a target class is provided (used for re-rolls) 35 | if (targetClass !== null) { 36 | const classNumber = parseInt(targetClass); 37 | if (!isNaN(classNumber) && targetClass !== 'nonstandard') { 38 | // Standard class (1-5) 39 | cardPool = cardPool.filter(card => card.class === classNumber); 40 | } else if (targetClass.toLowerCase() === 'nonstandard') { 41 | // Non-Standard class: filter cards where class is < 1 OR > 5 42 | cardPool = cardPool.filter(c => c.class < 1 || c.class > 5); 43 | } 44 | } 45 | 46 | if (cardPool.length === 0) return null; 47 | return cardPool[Math.floor(Math.random() * cardPool.length)]; 48 | } 49 | 50 | // --- Card Handling Functions --- 51 | 52 | /** 53 | * Handles the Amnesia event: 10% chance to remove 10 random cards if drop is blocked. 54 | * @returns {boolean} True if amnesia occurred. 55 | */ 56 | function handleAmnesia(userData, obj, message) { 57 | if (Math.random() < 0.10) { 58 | if (userData.cards.length === 0) return false; 59 | 60 | const cardsToRemoveCount = Math.min(10, userData.cards.length); 61 | const removedCardNames = []; 62 | 63 | for (let i = 0; i < cardsToRemoveCount; i++) { 64 | const cardIndex = Math.floor(Math.random() * userData.cards.length); 65 | const cardItem = userData.cards[cardIndex]; 66 | 67 | cardItem.count -= 1; 68 | removedCardNames.push(cardItem.name); 69 | 70 | if (cardItem.count <= 0) { 71 | userData.cards.splice(cardIndex, 1); 72 | } 73 | } 74 | 75 | SaveObjToDB(obj); 76 | 77 | const cardList = removedCardNames.map(name => `\`${name}\``).join(', '); 78 | const embed = new EmbedBuilder() 79 | .setColor(0xcc0000) 80 | .setTitle(`😱 ${LOCALES.DropCard__AmnesiaTitle[CONSTANTS.LANG] || "AMNESIA STRIKES!"} 😱`) 81 | .setDescription(`${LOCALES.DropCard__AmnesiaMessage[CONSTANTS.LANG] || "You tried too hard to drop a card and suffered memory loss! The following cards were lost:"}`) 82 | .addFields({ 83 | name: LOCALES.DropCard__AmnesiaLostCards[CONSTANTS.LANG] || "Lost Cards", 84 | value: cardList 85 | }) 86 | .setFooter({ text: LOCALES.DropCard__AmnesiaBonusDrop[CONSTANTS.LANG] || "Due to the shock, you receive a bonus drop." }); 87 | 88 | message.reply({ embeds: [embed] }); 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | 95 | /** 96 | * Core function to update user inventory with a new card. 97 | * Also sets the new 'rerollAvailable' flag on the user object. 98 | */ 99 | function updateInventory(userData, newCard) { 100 | let userCard = userData.cards.find(item => item.name === newCard.name); 101 | 102 | if (userCard) { 103 | userCard.count += 1; 104 | } else { 105 | userCard = { 106 | "name": newCard.name, 107 | "count": 1, 108 | "url": newCard.url 109 | }; 110 | userData.cards.push(userCard); 111 | } 112 | 113 | const sameCardCount = userCard.count; 114 | let reRollFlag = (sameCardCount > 0 && sameCardCount % 3 === 0); 115 | 116 | // Set flag on user data if a re-roll is earned 117 | if (reRollFlag) { 118 | userData.rerollAvailable = true; 119 | } 120 | 121 | return { sameCardCount, reRollFlag }; 122 | } 123 | 124 | /** 125 | * Displays the dropped card and handles the re-roll button collector. 126 | */ 127 | async function showGivenCard(message, card, reRollFlag, obj, userData, client, isBonusDrop = false) { 128 | // Determine the card class number for the button ID 129 | let cardData = obj.cards.find(cardDB => cardDB.name === card.name); 130 | let cardClassNumber = cardData?.class; 131 | 132 | // Use 'nonstandard' string for ID if class is outside 1-5 (BASE CLASS) 133 | const rerollIdClass = (cardClassNumber >= 1 && cardClassNumber <= 5) ? cardClassNumber : 'nonstandard'; 134 | 135 | let cardClassString = GetClassString(cardClassNumber); 136 | let sameCardCount = userData.cards.find(item => item.name === card.name)?.count || 1; 137 | 138 | const user = await client.users.fetch(message.author.id); 139 | 140 | const embed = new EmbedBuilder() 141 | .setColor(isBonusDrop ? "#33aaff" : "#d1b91f") 142 | .setAuthor({ name: user.username, iconURL: user.displayAvatarURL() }); 143 | 144 | let title; 145 | if (isBonusDrop) { 146 | title = LOCALES.DropCard__MessageEmbed__got_bonus_card[CONSTANTS.LANG] || "You got a bonus drop:"; 147 | } else { 148 | title = LOCALES.DropCard__MessageEmbed__got_card_with_name[CONSTANTS.LANG] || "You got a card named:"; 149 | } 150 | 151 | embed.setTitle(title); 152 | embed.setDescription(`**${(cardClassString) ? cardClassString : ReplaceEmojisFromNameToClass(card)} [${card.name}](${card.url})**`); 153 | embed.setFooter({ text: `${LOCALES.DropCard__MessageEmbed__cards_you_have_now[CONSTANTS.LANG]} ${sameCardCount}` }); 154 | 155 | const isVideo = card.url.toLowerCase().endsWith('.mp4'); 156 | 157 | if (isVideo) { 158 | // Add a field to the embed indicating a video was dropped (for clarity) 159 | embed.addFields({ 160 | name: `🎥 ${LOCALES.DropCard__VideoTitle[CONSTANTS.LANG]}`, 161 | value: `${LOCALES.DropCard__VideoMessage[CONSTANTS.LANG]}`, 162 | inline: false 163 | }); 164 | // Remove the image from the embed since it's a video 165 | embed.setImage(null); 166 | } else { 167 | // Standard image display 168 | embed.setImage(`${card.url}`); 169 | } 170 | 171 | let components = []; 172 | 173 | if (reRollFlag) { 174 | embed.addFields({ name: `✨ ${LOCALES.DropCard__MessageEmbed__3_cards_in_a_row1[CONSTANTS.LANG] || "Bonus Drop!"}`, 175 | value: `${LOCALES.DropCard__MessageEmbed__3_cards_in_a_row2[CONSTANTS.LANG] || "You get a free re-roll. Click the button below or call the drop command again!"}`, 176 | inline: false 177 | }); 178 | 179 | const row = new ActionRowBuilder().addComponents( 180 | new ButtonBuilder() 181 | // Embed the class ID (e.g., 'reroll_3' or 'reroll_nonstandard') 182 | .setCustomId(`reroll_${rerollIdClass}`) 183 | .setLabel(LOCALES.DropCard__RerollButton[CONSTANTS.LANG] || 'Reroll Card') 184 | .setStyle(ButtonStyle.Success) 185 | .setDisabled(false) 186 | ); 187 | components.push(row); 188 | } 189 | 190 | // Send the main embed/button message 191 | const messageReply = await message.reply({ 192 | embeds: [embed], 193 | components: components, 194 | }); 195 | 196 | // --- SECOND MESSAGE FOR PLAYABLE VIDEO LINK --- 197 | if (isVideo) { 198 | // Send a new, separate message containing only the MP4 link for the Discord player 199 | await message.channel.send(card.url); 200 | } 201 | // --- END SECOND MESSAGE --- 202 | 203 | 204 | // Start collector if reRollFlag is set 205 | if (reRollFlag) { 206 | const row = components[0]; 207 | const filter = (i) => i.customId.startsWith('reroll_') && i.user.id === message.author.id; 208 | const collector = messageReply.createMessageComponentCollector({ filter, time: COLLECTOR_TIMEOUT, componentType: ComponentType.Button }); 209 | 210 | collector.on('collect', async i => { 211 | await i.deferUpdate(); 212 | collector.stop('reroll_used'); 213 | 214 | // Consume the flag if the button is used instead of the command 215 | userData.rerollAvailable = false; 216 | SaveObjToDB(obj); 217 | 218 | // Extract the class ID (e.g., '3' or 'nonstandard') 219 | const rerollClass = i.customId.split('_')[1]; 220 | 221 | // Perform the re-roll logic (isReroll=true, isAmnesiaBonus=false) 222 | await handleCardDrop(message, obj, client, true, false, rerollClass); 223 | 224 | // Disable the re-roll button on the original message 225 | const disabledRow = new ActionRowBuilder().addComponents( 226 | ButtonBuilder.from(row.components[0]).setDisabled(true).setLabel(`${LOCALES.DropCard__RerollUsed[CONSTANTS.LANG] || 'Reroll Used'}`) 227 | ); 228 | await messageReply.edit({ components: [disabledRow] }).catch(() => {}); 229 | }); 230 | 231 | collector.on('end', async (collected, reason) => { 232 | if (reason === 'time' && messageReply.editable) { 233 | // If the user did nothing, the flag is still true, and we should clear it. 234 | if (userData.rerollAvailable) { 235 | userData.rerollAvailable = false; 236 | SaveObjToDB(obj); 237 | } 238 | const disabledRow = new ActionRowBuilder().addComponents( 239 | ButtonBuilder.from(components[0].components[0]).setDisabled(true).setLabel(`${LOCALES.DropCard__RerollExpired[CONSTANTS.LANG] || 'Reroll Expired'}`) 240 | ); 241 | await messageReply.edit({ components: [disabledRow] }).catch(() => {}); 242 | } 243 | }); 244 | } 245 | } 246 | 247 | /** 248 | * Handles the actual card dropping, timing, and database update. 249 | * @param {boolean} isReroll - True if this drop is triggered by the re-roll button (DOES NOT reset timer). 250 | * @param {boolean} isAmnesiaBonus - True if this drop is triggered by the amnesia event (RESETS timer). 251 | * @param {string|null} rerollClass - The class ID to limit the drop pool to during a re-roll. 252 | */ 253 | async function handleCardDrop(message, obj, client, isReroll = false, isAmnesiaBonus = false, rerollClass = null) { 254 | const userData = obj.users.find(i => i.id === message.author.id); 255 | if (!userData) { 256 | return message.reply("Error: User data not found. Please try again."); 257 | } 258 | 259 | // Drop is allowed if: it's a button re-roll, amnesia bonus, OR the 'rerollAvailable' flag is set. 260 | let canDrop = isReroll || isAmnesiaBonus || userData.rerollAvailable; 261 | let remainingTimeMessage = null; 262 | 263 | // Check cooldown ONLY if not a bonus drop 264 | if (!canDrop) { 265 | if (userData.lastDropDate === null) { 266 | canDrop = true; 267 | } else { 268 | const lastDropDate = new Date(userData.lastDropDate); 269 | 270 | if (!(configLocalTime[0]) && daysDiff(new Date(), lastDropDate) >= 1) { 271 | canDrop = true; 272 | } else if (configLocalTime[0] && new Date() >= ReturnRequeredUTCDateToReset()) { 273 | canDrop = true; 274 | } else { 275 | // Cooldown calculation 276 | let resetTime = (!(configLocalTime[0]) ? new Date(lastDropDate.getTime() + 24 * 3600000) : ReturnRequeredUTCDateToReset()); 277 | let remainingTime = resetTime - Date.now(); 278 | 279 | let remainingHours = Math.floor(remainingTime / 3600000); 280 | remainingTime -= remainingHours * 3600000; 281 | let remainingMinutes = Math.floor(remainingTime / 60000); 282 | remainingTime -= remainingMinutes * 60000; 283 | let remainingSecs = Math.floor(remainingTime / 1000); 284 | 285 | remainingTimeMessage = `${LOCALES.DropCard__MessageEmbed__cant_get_more_now[CONSTANTS.LANG]} ${remainingHours}${LOCALES.DropCard__MessageEmbed__hours[CONSTANTS.LANG]} ${remainingMinutes }${LOCALES.DropCard__MessageEmbed__min[CONSTANTS.LANG]} ${remainingSecs }${LOCALES.DropCard__MessageEmbed__sec[CONSTANTS.LANG]}`; 286 | } 287 | } 288 | } 289 | 290 | if (canDrop) { 291 | // If the manual drop is consuming the 'rerollAvailable' flag. 292 | if (userData.rerollAvailable) { 293 | userData.rerollAvailable = false; 294 | // Set isReroll to true for display purposes (shows "Bonus Drop"). 295 | isReroll = true; 296 | } 297 | 298 | // Get the card 299 | const rCard = GetRandomCard(obj.cards, rerollClass); 300 | 301 | if (!rCard) { 302 | if (isReroll) { 303 | return message.reply(`There are no other active cards of the same rarity to re-roll into.`); 304 | } 305 | return message.reply("Error: No active cards available to drop."); 306 | } 307 | 308 | // 1. Update Inventory and get results (This updates 'rerollAvailable' if a *new* reroll is earned) 309 | const { reRollFlag } = updateInventory(userData, rCard); 310 | 311 | // 2. Update Drop Date: 312 | // Timer resets only if it was a standard drop OR the Amnesia bonus drop. 313 | if (!isReroll) { 314 | userData.lastDropDate = new Date(); 315 | } 316 | 317 | // 3. Save, Show, and Process re-roll 318 | SaveObjToDB(obj); 319 | await showGivenCard(message, rCard, reRollFlag, obj, userData, client, isReroll || isAmnesiaBonus); 320 | 321 | } else { 322 | // Drop is blocked (Cooldown) 323 | if (!isReroll) { 324 | const amnesiaOccurred = handleAmnesia(userData, obj, message); 325 | 326 | if (amnesiaOccurred) { 327 | // Amnesia occurred: grant a bonus drop and reset the timer. 328 | await handleCardDrop(message, obj, client, false, true); 329 | return; 330 | } 331 | 332 | if (!amnesiaOccurred) { 333 | // No Amnesia: show the cooldown message. 334 | message.reply(remainingTimeMessage); 335 | } 336 | } 337 | } 338 | } 339 | 340 | 341 | const DropCard = (message, args, client) => { 342 | UserCheck(message.author.id); 343 | let obj = ReadDBFile(); 344 | if (!obj) return; 345 | 346 | const userData = obj.users.find(i => i.id === message.author.id); 347 | if (!userData) { 348 | return; 349 | } 350 | 351 | // Call the main logic handler (Standard drop attempt) 352 | handleCardDrop(message, obj, client, false, false); 353 | }; 354 | 355 | module.exports = { 356 | name: LOCALES.DropCard__EXPORTS__name[CONSTANTS.LANG], 357 | usage() { return `${CONSTANTS.PREFIX}${this.name}`; }, 358 | desc: LOCALES.DropCard__EXPORTS__desc[CONSTANTS.LANG], 359 | func: DropCard, 360 | }; -------------------------------------------------------------------------------- /constants/locales.js: -------------------------------------------------------------------------------- 1 | const LOCALES = { 2 | DropCard__MessageEmbed__got_card_with_name: { 3 | ru: `Вам выпала карта с названием: `, 4 | en: `You got a card with the name: `, 5 | jp: `あなたはカードを在ります: `, 6 | }, 7 | DropCard__MessageEmbed__cards_you_have_now: { 8 | ru: `Таких карт у вас сейчас: X`, 9 | en: `Such cards you have now: X`, 10 | jp: `今あなたのインベントリ内のこれ同じカードの数:`, 11 | }, 12 | DropCard__MessageEmbed__3_cards_in_a_row1: { 13 | ru: `Поздравляю, тебе выпало 3 повторки! 👏👏👏 `, 14 | en: `Congratulations, you have collected 3 identical cards👏👏👏`, 15 | jp: `おめでと、あなたは3枚同じカードがあります!👏👏👏`, 16 | }, 17 | DropCard__MessageEmbed__3_cards_in_a_row2: { 18 | ru: `Можешь попытаться выбить еще одну карту прямо сейчас!`, 19 | en: `You can try to get another card right now!`, 20 | jp: `今カードをもらってはもいいです!`, 21 | }, 22 | DropCard__MessageEmbed__cant_get_more_now: { 23 | ru: `Сейчас у вас не получится получить карту, но вы можете попытать удачу через:`, 24 | en: `Now you will not be able to get a card, but you can try your luck through:`, 25 | jp: `今すぐカードを貰っていけませんが1っ回をして時間の後もいいです:`, 26 | }, 27 | DropCard__MessageEmbed__hours: { 28 | ru: `ч`, 29 | en: `h`, 30 | jp: `時間` 31 | }, 32 | DropCard__MessageEmbed__min: { 33 | ru: `м`, 34 | en: `m`, 35 | jp: `分` 36 | }, 37 | DropCard__MessageEmbed__sec: { 38 | ru: `с`, 39 | en: `s`, 40 | jp: `秒` 41 | }, 42 | DropCard__EXPORTS__name: { 43 | ru: `дайкарту`, 44 | en: `drop`, 45 | jp: `貰います` 46 | }, 47 | DropCard__EXPORTS__desc: { 48 | ru: `Раз в 24 часа рандомная карта помещается вам в инвентарь при использовании этой команды`, 49 | en: `Once every 24 hours, a random card is placed in your inventory when using this command`, 50 | jp: `このコマンドを使用すると、24時間ごとにランダムなカードがインベントリに配置されます` 51 | }, 52 | DropCard__AmnesiaTitle: { 53 | ru: `Деменция прогресирует`, 54 | en: `Dementia is progressing`, 55 | jp: `認知症が進行している` 56 | }, 57 | DropCard__AmnesiaMessage: { 58 | ru: `В попытках выбить карту ты забыл что у тебя были следующие карты:`, 59 | en: `In your attempts to knock out a card, you forgot that you had the following cards:`, 60 | jp: `カードをノックアウトしようとするときに、次のカードがあることを忘れました:` 61 | }, 62 | DropCard__AmnesiaLostCards: { 63 | ru: `Забытые карты:`, 64 | en: `Lost Cards:`, 65 | jp: `忘れられたカード:` 66 | }, 67 | DropCard__AmnesiaBonusDrop: { 68 | ru: `Осознав это ты получаешь еще одну карту.`, 69 | en: `Due to the shock, you receive a bonus drop.`, 70 | jp: `ショックにより、ボーナスドロップを受け取ります。` 71 | }, 72 | DropCard__MessageEmbed__got_reroll_card: { 73 | ru: `Вы рерольнули и получили: `, 74 | en: `You re-rolled and got: `, 75 | jp: `再ロールで得たもの: `, 76 | }, 77 | DropCard__MessageEmbed__got_bonus_card: { // Used for Amnesia bonus drop or any generic bonus 78 | ru: `Вы получили бонусную карту: `, 79 | en: `You got a bonus card: `, 80 | jp: `ボーナスカードを手に入れた: `, 81 | }, 82 | DropCard__RerollButton: { 83 | ru: `Бонусный ролл того же класса карточки`, 84 | en: `Reroll Card`, 85 | jp: `カードを再ロールする`, 86 | }, 87 | DropCard__RerollUsed: { 88 | ru: `Бонусный ролл использован`, 89 | en: `Reroll Used`, 90 | jp: `再ロールを使用済み`, 91 | }, 92 | DropCard__RerollExpired: { 93 | ru: `Бонусный ролл истёк`, 94 | en: `Reroll Expired`, 95 | jp: `再ロールが期限切れ`, 96 | }, 97 | DropCard__VideoTitle: { 98 | ru: `Выпала видео карточка`, 99 | en: `Video Card Dropped`, 100 | jp: `ビデオカードを落とした`, 101 | }, 102 | DropCard__VideoMessage: { 103 | ru: `Видео контент показан в следующем сообщении.`, 104 | en: `The video content is displayed in the following message.`, 105 | jp: `ビデオコンテンツは次のメッセージに表示されます。`, 106 | }, 107 | 108 | 109 | Profile__MessageEmbed__wrong_user: { 110 | ru: `Для просмотра профиля учаcтника необходимо упомянуть только его`, 111 | en: `To view a participant's profile, you only need to mention him`, 112 | jp: `参加者のプロフィールを表示するには、彼に言及するだけです` 113 | }, 114 | Profile__MessageEmbed__user_profile: { 115 | ru: `Профиль участника`, 116 | en: `Member profile of`, 117 | jp: `メンバープロフィール`, 118 | }, 119 | Profile__MessageEmbed__cards_fallen_total: { 120 | ru: ` Сколько всего карт выпало :`, 121 | en: ` How many cards have fallen out in total :`, 122 | jp: ` 落ちたカードの枚数`, 123 | }, 124 | Profile__MessageEmbed__statistics_of_dropped_cards: { 125 | ru: ` Статистика выпавших карт :`, 126 | en: ` Statistics of dropped cards :`, 127 | jp: ` あなたのカード統計: `, 128 | }, 129 | Profile__MessageEmbed__collected_non_standard_cards: { 130 | ru: ` Собрано нестандартных карт :`, 131 | en: ` Collected non-standard cards :`, 132 | jp: ` 非標準カード: `, 133 | }, 134 | Profile__MessageEmbed__not_been_opened_yet: { 135 | ru: ` Сколько карт еще не открыто :`, 136 | en: ` How many cards have not been opened yet :`, 137 | jp: ` カード枚数がまだ研究しません : `, 138 | }, 139 | Profile__MessageEmbed__fell_out_the_most_times: { 140 | ru: ` Карта, которая больше всего раз выпала :`, 141 | en: ` The card that fell out the most times :`, 142 | jp: `これはあなたのカード一番多い枚数があります`, 143 | }, 144 | Profile__MessageEmbed__no_cards_in_the_inventory: { 145 | ru: ` на данный момент не имеет карт в инвентаре:`, 146 | en: ` currently has no cards in the inventory :`, 147 | jp: ` は今カードを在りません!`, 148 | }, 149 | Profile__MessageEmbed__of: { 150 | ru: `из`, 151 | en: `of`, 152 | jp: `/`, 153 | }, 154 | Profile__EXPORTS__name: { 155 | ru: `профиль`, 156 | en: `profile`, 157 | jp: `プロフィール`, 158 | }, 159 | Profile__EXPORTS__desc: { 160 | ru: `Показывает профиль пользователя, содержащий информацию о статистике выпавших ему карт`, 161 | en: `Shows the user's profile containing information about the statistics of the cards that fell to him`, 162 | jp: `ユーザーのプロフィールとユーザーのカード統計を見せます`, 163 | }, 164 | 165 | GiveCard__MessageEmbed__issued_a_card: { 166 | ru: `Вами была выдана карта с названием: `, 167 | en: `You have been issued a card with the name: `, 168 | jp: `あなたはカードをもらいました:`, 169 | }, 170 | GiveCard__MessageEmbed__wrong_user: { 171 | ru: `Для выдачи карты учаcтнику необходимо упомянуть только его`, 172 | en: `To give card to participant, you only need to mention him `, 173 | jp: `ユーザーが正しく指定されていません`, 174 | }, 175 | GiveCard__EXPORTS__name: { 176 | ru: `выдайкарту`, 177 | en: `giveacard`, 178 | jp: `カードを上げます`, 179 | }, 180 | GiveCard__EXPORTS__desc: { 181 | ru: `Выдает карту указанному пользователю :warning: `, 182 | en: `Issues the card to the specified user :warning: `, 183 | jp: `ユーザーにカードをあげます :warning:`, 184 | }, 185 | 186 | ActivateCode__MessageEmbed__code_expired: { 187 | ru: `Время действия введенного кода истекло`, 188 | en: `The entered code has expired`, 189 | jp: `入力したコードの有効期限が切れています `, 190 | }, 191 | ActivateCode__MessageEmbed__exceeded_number_uses: { 192 | ru: `превышено количество использований введенного вами кода`, 193 | en: `exceeded the number of uses of the code you entered`, 194 | jp: `入力したコードの使用回数を超えました`, 195 | }, 196 | ActivateCode__MessageEmbed__already_used: { 197 | ru: `Вами уже был использован данный код`, 198 | en: `You have already used this code`, 199 | jp: `もうあなたはこのコードを使いました`, 200 | }, 201 | ActivateCode__MessageEmbed__code: { 202 | ru: `код: `, 203 | en: `code: `, 204 | jp: `コード`, 205 | }, 206 | ActivateCode__MessageEmbed__activated: { 207 | ru: ` успешно активирован!`, 208 | en: ` has been successfully activated!`, 209 | jp: ` を活性化ました!`, 210 | }, 211 | ActivateCode__EXPORTS__name: { 212 | ru: `активируй`, 213 | en: `activate`, 214 | jp: `活性化`, 215 | }, 216 | ActivateCode__EXPORTS__desc: { 217 | ru: `Активирует эвентовый код позволяющий получить шанс на крутку`, 218 | en: `Activates an event code that allows you to get a chance to spin`, 219 | jp: `コードを有効にするとタイマーがリセットされ、ユーザーは別のカードを受け取る機会が与えられます`, 220 | }, 221 | 222 | CreateCode__MessageEmbed__created_code_with_name: { 223 | ru: `Вами был создан код с названием: `, 224 | en: `You have created a code with the name: `, 225 | jp: `コードを作りました:`, 226 | }, 227 | CreateCode__MessageEmbed__able_to_use_it: { 228 | ru: `Использовать его смогут `, 229 | en: `Count of users able to use it: `, 230 | jp: `このコードを使用できるユーザーの数:`, 231 | }, 232 | CreateCode__MessageEmbed__unlimited_quantity: { 233 | ru: `неограниченное количество`, 234 | en: `unlimited`, 235 | jp: `無制限の量`, 236 | }, 237 | CreateCode__MessageEmbed__just_unlimited: { 238 | ru: `неограничено`, 239 | en: `unlimited`, 240 | jp: `いいえ`, 241 | }, 242 | CreateCode__MessageEmbed__users: { 243 | ru: `пользователей`, 244 | en: ``, 245 | jp: `人`, 246 | }, 247 | CreateCode__MessageEmbed__code_expiration_date: { 248 | ru: `Дата истечения работы кода: `, 249 | en: `Code expiration date:`, 250 | jp: `コードの有効期限:`, 251 | }, 252 | CreateCode__EXPORTS__name: { 253 | ru: `создатькод`, 254 | en: `createcode`, 255 | jp: `コードを作ります`, 256 | }, 257 | CreateCode__EXPORTS__desc: { 258 | ru: `Создает код, который можно активировать для получения возможности крутки карт :warning:`, 259 | en: `Creates a code that can be activated to get the ability to drop cards:warning:`, 260 | jp: `カードを受け取るために有効化できるコードを生成します `, 261 | }, 262 | 263 | DeleteCard__MessageEmbed__deleted_card_with_name: { 264 | ru: `Вами была удалена карта с текущим названием: `, 265 | en: `You have deleted a card with the current name:`, 266 | jp: `カードを削除ました:`, 267 | }, 268 | DeleteCard__MessageEmbed__card_not_found: { 269 | ru: `Не найдено указанной карты!`, 270 | en: `The specified card was not found!`, 271 | jp: `このカードは見つかりませんでした!`, 272 | }, 273 | DeleteCard__MessageEmbed__mandatory_argument: { 274 | ru: `Для функции требуется 1 обязательный аргумент - полное название карты!`, 275 | en: `The function requires 1 mandatory argument - the full name of the card!`, 276 | jp: `関数にはカードの完全な名前が必要です!`, 277 | }, 278 | DeleteCard__EXPORTS__name: { 279 | ru: `удалитькарту`, 280 | en: `deletecard`, 281 | jp: `カードを削除`, 282 | }, 283 | DeleteCard__EXPORTS__desc: { 284 | ru: `Удаляет карту из общего пула и у всех пользователей :warning:`, 285 | en: `Removes the card from the shared pool and from all users :warning:`, 286 | jp: `どこからでもカードを削除します`, 287 | }, 288 | 289 | AddNewCard__MessageEmbed__added_card_with_name: { 290 | ru: `Вами была добавлена карта с названием: `, 291 | en: `You have added a card with the name: `, 292 | jp: `新しいカードを追加しました`, 293 | }, 294 | AddNewCard__MessageEmbed__name_already_exists: { 295 | ru: `Такое название карты уже существует!`, 296 | en: `This cardname already exists!`, 297 | jp: `このカード名前はもうありました!`, 298 | }, 299 | AddNewCard__MessageEmbed__class_number: { 300 | ru: `Класс карты должен быть числом!`, 301 | en: `The card class must be a number!`, 302 | jp: `カードクラスは数字でなければなりません!`, 303 | }, 304 | AddNewCard__MessageEmbed__media_not_found: { 305 | ru: `Не указана ссылка на изображение и не найдено вложенного файла`, 306 | en: `The link to the image is not specified or the attached file is not found`, 307 | jp: `画像リンクが提供されておらず、添付ファイルが見つかりません`, 308 | }, 309 | AddNewCard__MessageEmbed__media_incorrect: { 310 | ru: `Неправильно указана ссылка на изображение | неверный прикреплекнный файл`, 311 | en: `The link to the image is incorrect | the attached file is incorrect`, 312 | jp: `間違った画像リンク | 無効な添付ファイル`, 313 | }, 314 | AddNewCard__EXPORTS__name: { 315 | ru: `новаякарта`, 316 | en: `addcard`, 317 | jp: `新しいカード`, 318 | }, 319 | AddNewCard__EXPORTS__desc: { 320 | ru: `Добавляет новую карту в пул карточек которые могут выпадать игрокам :warning:`, 321 | en: `Adds a new card to the pool of cards that can be get by users :warning:`, 322 | jp: `ユーザーが取得できるカードのプールに新しいカードを追加します :warning:`, 323 | }, 324 | 325 | EditCard__MessageEmbed__edited_card_with_name: { 326 | ru: `Вами была изменена карта с текущим названием: `, 327 | en: `You have changed a card with the name: `, 328 | jp: `カードを変更しました:`, 329 | }, 330 | EditCard__MessageEmbed__class_number: this.AddNewCard__MessageEmbed__class_number, 331 | EditCard__MessageEmbed__media_not_found: this.AddNewCard__MessageEmbed__media_not_found, 332 | EditCard__MessageEmbed__media_incorrect: this.AddNewCard__MessageEmbed__media_incorrect, 333 | EditCard__EXPORTS__name: { 334 | ru: `изменитькарту`, 335 | en: `editcard`, 336 | jp: `カードを変更`, 337 | }, 338 | EditCard__EXPORTS__desc: { 339 | ru: `Меняет данные карточки в системе :warning:`, 340 | en: `Changes card data in the system :warning:`, 341 | jp: `システム内のカードデータを変更します :warning:`, 342 | }, 343 | 344 | ResetDrop__MessageEmbed__specify_user: { 345 | ru: `Укажите пользователя используя @`, 346 | en: `Specify the user using @`, 347 | jp: `@を使用してユーザーを指定します`, 348 | }, 349 | ResetDrop__MessageEmbed__to_all_users: { 350 | ru: ` всех пользователей!`, 351 | en: ` all users!`, 352 | jp: `すべてのユーザー`, 353 | }, 354 | ResetDrop__MessageEmbed__updated_drops: { 355 | ru: `Вами были обновлены крутки для`, 356 | en: `You have updated the drops for `, 357 | jp: `カードを受け取るためのタイマーをリセットします:`, 358 | }, 359 | ResetDrop__EXPORTS__name: { 360 | ru: `обновикрутки`, 361 | en: `resetdrop`, 362 | jp: `タイマーリセット`, 363 | }, 364 | ResetDrop__EXPORTS__desc: { 365 | ru: `Обнуляет счетчик круток всем/указанному пользователю :warning: `, 366 | en: `Resets the drops to all/specified user :warning:`, 367 | jp: `全ユーザーまたは指定ユーザーのカード受け取りタイマーをリセットします :warning:`, 368 | }, 369 | 370 | ShowCards__MessageEmbed__no_cards: { 371 | ru: `Пока что у вас нет ни одной выбитой карты в инвентаре.`, 372 | en: `You don't have a single knocked-out card in your inventory.`, 373 | jp: `まだあなたはカードがありっていません`, 374 | }, 375 | ShowCards__MessageEmbed__cards_in_inventary1: { 376 | ru: `Вот что у `, 377 | en: `That's what `, 378 | jp: `これはユーザーが有っています:`, 379 | }, 380 | ShowCards__MessageEmbed__cards_in_inventary2: { 381 | ru: `вас`, 382 | en: `you`, 383 | jp: ``, 384 | }, 385 | ShowCards__MessageEmbed__cards_in_inventary3: { 386 | ru: ` в инвентаре:`, 387 | en: `have:`, 388 | jp: ``, 389 | }, 390 | ShowCards__MessageEmbed__page: { 391 | ru: ` страница `, 392 | en: `page`, 393 | jp: `ページ`, 394 | }, 395 | ShowCards__MessageEmbed__inventory_is_over: { 396 | ru: `Время действия инвентаря закончилось`, 397 | en: `Inventory expiration time is over`, 398 | jp: `在庫の有効期限が切れています`, 399 | }, 400 | ShowCards__MessageEmbed__incorrect_user: { 401 | ru: `Для просмотра инвентаря участника необходимо упомянуть только его`, 402 | en: `To view the participant's inventory, you only need to mention him`, 403 | jp: `ユーザーが正しく指定されていません`, 404 | }, 405 | ShowCards__MessageEmbed__access_denied: { 406 | ru: `Вы не можете посмотреть инвентарь участника пока он сам его не откроет при вас`, 407 | en: `You cannot view the participant's inventory until he opens it in chat to you`, 408 | jp: `他のユーザーの在庫をオンデマンドで閲覧することはできません`, 409 | }, 410 | ShowCards__MessageEmbed__no_cards2: { 411 | ru: `у участника`, 412 | en: `the user does not`, 413 | jp: ``, 414 | }, 415 | ShowCards__MessageEmbed__no_cards3: { 416 | ru: `у вас`, 417 | en: `you don't`, 418 | jp: ``, 419 | }, 420 | ShowCards__MessageEmbed__no_cards4: { 421 | ru: `нет ни одной выбитой карты в инвентаре.`, 422 | en: `have a card in inventory.`, 423 | jp: `ユーザーはかーどーが有っていません`, 424 | }, 425 | ShowCards__MessageEmbed__total: { 426 | ru: `Общее количество уникальных карт`, 427 | en: `Total unique cards collected`, 428 | jp: `収集されたユニークカードの合計`, 429 | }, 430 | ShowCards__MessageEmbed__category: { 431 | ru: `Категория`, 432 | en: `Category`, 433 | jp: `カテゴリ`, 434 | }, 435 | ShowCards__EXPORTS__name: { 436 | ru: `покажимне`, 437 | en: `show`, 438 | jp: `見せて`, 439 | }, 440 | ShowCards__EXPORTS__desc: { 441 | ru: `Показывает карты, находящиеся у вас или у @UserMention в инвентаре`, 442 | en: `Shows the cards that you or @UserMention have in inventory`, 443 | jp: `あなたまたは@ユーザーのカードを見せます`, 444 | }, 445 | ShowCards__BTN_info: { 446 | ru: `Нажми на кнопку с названием чтобы просмотреть карточку`, 447 | en: `Click a button to view the card`, 448 | jp: `ボタンをクリックしてカードを表示してください`, 449 | }, 450 | ShowCards__VideoCardViewer: { 451 | ru: `Просмотр видео карточки`, 452 | en: `Video Card Viewer`, 453 | jp: `ビデオカードビューア`, 454 | }, 455 | ShowCards__VideoCardViewerBelow: { 456 | ru: `Видео карточка отобразится сообщением ниже`, 457 | en: `The video player is displayed in the message below.`, 458 | jp: `ビデオ プレーヤーは以下のメッセージに表示されます。`, 459 | }, 460 | ShowCards__CardView: { 461 | ru: `Просмотр карты`, 462 | en: `Viewing Card`, 463 | jp: `視聴カード`, 464 | }, 465 | ShowCards__PermissionMessage: { 466 | ru: `У вас нет прав нажимать эту кнопку`, 467 | en: `You do not have permission to use this button.`, 468 | jp: `このボタンを使用する権限がありません。`, 469 | }, 470 | 471 | Undiscovered__MessageEmbed__no_cards_in_base: { 472 | ru: `Сначала добавьте карты в базу, перед тем как заставлять меня считать то, чего нет!`, 473 | en: `First add the cards to the database before forcing me to count what is not there!`, 474 | jp: `DBにカードがありまっていません`, 475 | }, 476 | Undiscovered__MessageEmbed__no_users: { 477 | ru: `Не у кого считать карты!`, 478 | en: `There is no one to count the cards!`, 479 | jp: `DBにユーザーがありまっていません!`, 480 | }, 481 | Undiscovered__MessageEmbed__cards_untouched: { 482 | ru: `На данный момент количество карт, которых не повидал сервер: `, 483 | en: `At the moment, the number of cards that the server has not seen: `, 484 | jp: `カード枚数みんなはみませんでした:`, 485 | }, 486 | Undiscovered__EXPORTS__name: { 487 | ru: `неисследовано`, 488 | en: `undiscovered`, 489 | jp: `研究しませんでした`, 490 | }, 491 | Undiscovered__EXPORTS__desc: { 492 | ru: `Показывает количество карт, которых нет ни у одного из участников`, 493 | en: `Shows the number of cards that none of the participants have`, 494 | jp: `カードのかずを` 495 | }, 496 | 497 | FindCardByName__MessageEmbed_one_more_card_exist: { 498 | ru: `Уточните название карты тк есть больше одного совпадения`, 499 | en: `Specify the full name of the card, there is more than one match`, 500 | jp: `` 501 | }, 502 | FindCardByName__MessageEmbed_no_similar_name_found: { 503 | ru: `Не найдено похожего названия`, 504 | en: `No similar name found`, 505 | jp: `類似の名前は見つかりませんでした` 506 | }, 507 | RegisterUser__MessageEmbed_registered: { 508 | ru: `зарегистрирован`, 509 | en: `registered`, 510 | jp: `は登録た` 511 | }, 512 | UserCheck__MessageEmbed_db_error: { 513 | ru: `Ошибка чтения базы`, 514 | en: `Database reading error`, 515 | jp: `DBの読み方の過ち` 516 | }, 517 | Help__MessageEmbed_commands: { 518 | ru: `Команды бота`, 519 | en: `Bot Commands`, 520 | jp: `ボットのコマンド` 521 | }, 522 | Help__EXPORTS__name: { 523 | ru: `помощь`, 524 | en: `help`, 525 | jp: `手伝い` 526 | }, 527 | Help__EXPORTS__desc: { 528 | ru: `Показывает какие команды имеются у бота`, 529 | en: `Shows which commands the bot has`, 530 | jp: `ボットのコマンドを見せる` 531 | }, 532 | } 533 | 534 | module.exports = LOCALES; -------------------------------------------------------------------------------- /commands/ShowCards.js: -------------------------------------------------------------------------------- 1 | const UserCheck = require("../utils/UserCheck.js"); 2 | const ReadDBFile = require("../utils/ReadDBFile.js"); 3 | const CONSTANTS = require ("../constants/constants.js"); 4 | const SaveObjToDB = require("../utils/SaveObjToDB.js"); 5 | const { 6 | EmbedBuilder, 7 | ActionRowBuilder, 8 | ButtonBuilder, 9 | ButtonStyle, 10 | ComponentType, 11 | PermissionsBitField 12 | } = require('discord.js'); 13 | const ReplaceEmojisFromNameToClass = require("../utils/ClassFromName.js"); 14 | const GetClassString = require("../utils/GetClassString.js"); 15 | const LOCALES = require('../constants/locales.js'); 16 | 17 | // --- EMOJI HELPERS & CONSTANTS --- 18 | 19 | /** 20 | * Returns a single Unicode emoji for use in ButtonBuilder.setEmoji() 21 | * Also defines the visual rarity mapping. 22 | * @param {number} cardClass - The class number (1-5). 23 | * @returns {string} A single Unicode emoji. 24 | */ 25 | function GetButtonEmoji(cardClass) { 26 | switch (cardClass) { 27 | case 1: 28 | return '⚪'; // Common 29 | case 2: 30 | return '🟢'; // Uncommon 31 | case 3: 32 | return '🔵'; // Rare 33 | case 4: 34 | return '🔴'; // Epic 35 | case 5: 36 | return '🟡'; // Legendary 37 | default: 38 | return '❓'; 39 | } 40 | } 41 | 42 | /** 43 | * Returns the descriptive rarity name. 44 | * @param {number} cardClass - The class number (1-5). 45 | * @returns {string} The rarity name. 46 | */ 47 | function GetRarityName(cardClass) { 48 | switch (cardClass) { 49 | case 1: return 'Common'; 50 | case 2: return 'Uncommon'; 51 | case 3: return 'Rare'; 52 | case 4: return 'Epic'; 53 | case 5: return 'Legendary'; 54 | default: return `Class ${cardClass}`; // Fallback for safety 55 | } 56 | } 57 | 58 | 59 | const NON_STANDARD_LABEL = 'Non-Standard'; 60 | const NON_STANDARD_EMOJI = '✨'; // Safe Unicode emoji for Non-Standard 61 | 62 | // --- Utility Functions --- 63 | 64 | /** 65 | * FIXED: Removes all Unicode emojis, custom Discord emojis, and common Discord text shortcuts from a string. 66 | */ 67 | function stripEmojis(str) { 68 | // 1. Explicitly remove common Discord text shortcuts that render as emojis 69 | let cleanedStr = str.replace(/:six_pointed_star:/g, ''); 70 | cleanedStr = cleanedStr.replace(/:skull:/g, ''); 71 | 72 | // 2. Custom Discord Emojis (e.g., or <:name:id>) 73 | const discordCustomRegex = //g; 74 | cleanedStr = cleanedStr.replace(discordCustomRegex, ''); 75 | 76 | // 3. Unicode Emojis (standard and complex) 77 | const unicodeRegex = /([\u{1F600}-\u{1F6FF}\u{1F300}-\u{1F5FF}\u{1F900}-\u{1F9FF}\u{1F680}-\u{1F6FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]|[\u{1F1E6}-\u{1F1FF}]{2})/gu; 78 | cleanedStr = cleanedStr.replace(unicodeRegex, '').trim(); 79 | 80 | // 4. Remove the extra text that sometimes accompanies the emojis in your card names 81 | cleanedStr = cleanedStr.replace(/six_pointed_star/g, ''); 82 | cleanedStr = cleanedStr.replace(/skull/g, ''); 83 | cleanedStr = cleanedStr.replace(/📖/g, ''); 84 | cleanedStr = cleanedStr.replace(/🏳️‍🌈/g, ''); 85 | 86 | return cleanedStr.trim(); 87 | } 88 | 89 | function GetUserCards(userId) { 90 | let obj = ReadDBFile(); 91 | // Assuming obj.users[].cards is an array of objects: [{ name: 'Card Name', count: N, url: '...' }] 92 | return obj.users.find(user => userId == user.id)?.cards || []; 93 | } 94 | 95 | /** 96 | * FIXED: Gets user cards and details them with class, URL, and a unique ID (index in dbObj.cards). 97 | * Ensures URL is always pulled from the master DB if not present in user inventory for video checks. 98 | * If a card is not found in the master DB, it is assigned class 0 (Non-Standard). 99 | */ 100 | function getDetailedUserCards(userCards, dbObj) { 101 | return userCards.map(userCard => { 102 | // Find the index in the master list for a unique ID 103 | const dbCardIndex = dbObj.cards.findIndex(cardDB => cardDB.name == userCard.name); 104 | const dbCard = dbObj.cards[dbCardIndex]; 105 | 106 | const cardClassNumber = dbCard ? dbCard.class : 0; 107 | // Assuming GetClassString is implemented to handle 0/nonstandard classes 108 | const cardClassString = GetClassString(cardClassNumber); 109 | 110 | // Explicitly use the URL from the user card, but fall back to the master DB card URL 111 | const cardUrl = userCard.url || (dbCard ? dbCard.url : null); 112 | 113 | return { 114 | ...userCard, 115 | uniqueId: dbCardIndex, // The unique numerical ID of the card TYPE in the master DB 116 | class: cardClassNumber, 117 | classString: cardClassString, 118 | dbCard: dbCard, 119 | url: cardUrl // Guaranteed URL 120 | }; 121 | }); 122 | } 123 | 124 | // --- Category View Renderer --- 125 | 126 | /** 127 | * Creates the Embed and ActionRow for the Category List View. 128 | */ 129 | function createCategoryEmbed(memberId, memberIsAuthor, dbObj) { 130 | const userCards = GetUserCards(memberId); 131 | const detailedCards = getDetailedUserCards(userCards, dbObj); 132 | 133 | const embed = new EmbedBuilder() 134 | .setColor(0x0f3961) 135 | .setTitle(`**${LOCALES.ShowCards__MessageEmbed__cards_in_inventary1[CONSTANTS.LANG]} ${(!memberIsAuthor) ? `<@${memberId}>` : `${LOCALES.ShowCards__MessageEmbed__cards_in_inventary2[CONSTANTS.LANG]}` } ${LOCALES.ShowCards__MessageEmbed__cards_in_inventary3[CONSTANTS.LANG]}**`); 136 | 137 | let fieldContent = ""; 138 | const totalCardCount = dbObj.cards.length; 139 | let buttonRows = []; 140 | let currentRow = new ActionRowBuilder(); 141 | 142 | // 1. Standard Classes (1 to 5) 143 | for (let cardClass = 1; cardClass <= 5; cardClass++) { 144 | const totalInClass = dbObj.cards.filter(c => c.class == cardClass).length; 145 | // FIX: Ensure detailedCards are filtered correctly by class 146 | const userInClass = detailedCards.filter(c => c.class == cardClass).length; 147 | 148 | const classStringForEmbed = GetClassString(cardClass); 149 | const buttonEmoji = GetButtonEmoji(cardClass); 150 | const rarityName = GetRarityName(cardClass); 151 | 152 | const classButtonLabel = `${rarityName} (${userInClass})`; 153 | 154 | fieldContent += `${classStringForEmbed}: **${userInClass} / ${totalInClass}**\n`; 155 | 156 | const classButton = new ButtonBuilder() 157 | .setCustomId(`category_${cardClass}`) 158 | .setLabel(classButtonLabel) 159 | .setEmoji(buttonEmoji) 160 | .setStyle(ButtonStyle.Secondary) 161 | .setDisabled(userInClass == 0); 162 | 163 | currentRow.addComponents(classButton); 164 | 165 | if (currentRow.components.length == 5) { 166 | buttonRows.push(currentRow); 167 | currentRow = new ActionRowBuilder(); 168 | } 169 | } 170 | 171 | // Push the partially filled standard row 172 | if (currentRow.components.length > 0) { 173 | buttonRows.push(currentRow); 174 | currentRow = new ActionRowBuilder(); 175 | } 176 | 177 | // 2. Non-Standard Cards (Class < 1 or Class > 5) 178 | const nonStandardFilter = (c) => c.class < 1 || c.class > 5; 179 | const totalNonStandard = dbObj.cards.filter(nonStandardFilter).length; 180 | const userNonStandard = detailedCards.filter(nonStandardFilter).length; 181 | 182 | if (totalNonStandard > 0 || userNonStandard > 0) { // FIX: Show non-standard if user HAS one, even if master DB doesn't list it 183 | fieldContent += `\n${NON_STANDARD_EMOJI} ${NON_STANDARD_LABEL}: **${userNonStandard} / ${totalNonStandard}**\n`; 184 | 185 | const nonStandardButton = new ButtonBuilder() 186 | .setCustomId('category_nonstandard') 187 | .setLabel(`${NON_STANDARD_LABEL} (${userNonStandard})`) 188 | .setEmoji(NON_STANDARD_EMOJI) 189 | .setStyle(ButtonStyle.Secondary) 190 | .setDisabled(userNonStandard == 0); 191 | 192 | currentRow.addComponents(nonStandardButton); 193 | } 194 | 195 | // Push the final row (containing non-standard button if applicable) 196 | if (currentRow.components.length > 0) { 197 | buttonRows.push(currentRow); 198 | } 199 | 200 | embed.setDescription(fieldContent); 201 | embed.addFields({ 202 | name: `**${LOCALES.ShowCards__MessageEmbed__total[CONSTANTS.LANG]}:**`, 203 | value: `**${detailedCards.length} / ${totalCardCount}**` // Use detailedCards.length for total unique cards owned 204 | }); 205 | 206 | return { embed, rows: buttonRows }; 207 | } 208 | 209 | // --- Card Viewer Renderer --- 210 | 211 | /** 212 | * Creates the Embed and ActionRow for the Single Card Viewer. 213 | * @param {Object} card - The detailed card object (from getDetailedUserCards). 214 | * @param {string} classId - The current class ID (e.g., '3' or 'nonstandard') to pass back. 215 | * @param {number} pageIndex - The current page index to pass back. 216 | */ 217 | function createCardViewerEmbed(card, classId, pageIndex) { 218 | const embed = new EmbedBuilder() 219 | .setColor(0x0f3961) 220 | .setTitle(`${LOCALES.ShowCards__CardView[CONSTANTS.LANG]}: ${card.name}`) // Shows full name including emojis 221 | .setDescription(`**${card.classString || ReplaceEmojisFromNameToClass(card)} (x${card.count})**`); 222 | 223 | let videoLink = null; 224 | const urlLower = card.url ? card.url.toLowerCase() : ''; 225 | 226 | // Check if it's a video file (.mp4) 227 | const isVideo = urlLower.endsWith('.mp4'); 228 | 229 | if (isVideo) { 230 | // Prepare the video link for the second message 231 | videoLink = card.url; 232 | 233 | embed.addFields({ 234 | name: `🎥 ${LOCALES.ShowCards__VideoCardViewer[CONSTANTS.LANG]}`, 235 | value: `${LOCALES.ShowCards__VideoCardViewerBelow[CONSTANTS.LANG]}`, 236 | inline: false 237 | }); 238 | embed.setImage(null); // Explicitly remove any placeholder image 239 | } else { 240 | // Standard image or GIF display (Discord handles GIFs in the embed image field) 241 | embed.setImage(`${card.url}`); 242 | } 243 | 244 | // Navigation buttons: Back to List View (of the class) and Delete 245 | const row = new ActionRowBuilder().addComponents( 246 | new ButtonBuilder() 247 | .setCustomId(`back_to_card_list_${classId}_${pageIndex}`) // Back button includes state 248 | .setLabel('List ↩️') 249 | .setStyle(ButtonStyle.Success), 250 | new ButtonBuilder() 251 | .setCustomId('delete_inventory') 252 | .setLabel('❌') 253 | .setStyle(ButtonStyle.Danger) 254 | ); 255 | 256 | return { embed, row, videoLink }; 257 | } 258 | 259 | 260 | // --- Card View Renderer --- 261 | 262 | /** 263 | * Creates the Embed and ActionRow for the filtered Card List View. 264 | */ 265 | function createCardEmbed(memberId, cardClass, pageIndex, dbObj) { 266 | const userCards = GetUserCards(memberId); 267 | const detailedCards = getDetailedUserCards(userCards, dbObj); 268 | 269 | // 1. Filter the cards based on the requested class 270 | let filteredCards; 271 | let className; 272 | 273 | if (cardClass == 'nonstandard') { 274 | filteredCards = detailedCards.filter(c => c.class < 1 || c.class > 5); 275 | className = "Non-Standard Cards"; 276 | } else { 277 | const classNumber = parseInt(cardClass); 278 | filteredCards = detailedCards.filter(c => c.class == classNumber); 279 | className = GetClassString(classNumber); 280 | } 281 | 282 | // FIX: Ensure filteredCards contains only UNIQUE card types (by uniqueId/index) 283 | const uniqueCardsMap = new Map(); 284 | filteredCards.forEach(card => { 285 | // Use uniqueId (index in dbObj.cards) as the key to guarantee uniqueness by card TYPE 286 | uniqueCardsMap.set(card.uniqueId, card); 287 | }); 288 | filteredCards = Array.from(uniqueCardsMap.values()); 289 | 290 | const pageCount = Math.ceil(filteredCards.length / CONSTANTS.PAGE_SIZE); 291 | 292 | if(filteredCards.length == 0) { 293 | return { 294 | embed: new EmbedBuilder().setDescription(`No cards found in category: ${className}`), 295 | rows: [new ActionRowBuilder()], 296 | pageCount: 0 297 | }; 298 | } 299 | 300 | const start = CONSTANTS.PAGE_SIZE * pageIndex; 301 | const end = CONSTANTS.PAGE_SIZE * (pageIndex + 1); 302 | 303 | const embed = new EmbedBuilder() 304 | .setColor(0x0f3961) 305 | .setTitle(`${LOCALES.ShowCards__MessageEmbed__category[CONSTANTS.LANG]}: ${className}`); 306 | 307 | // --- UPDATED: Use components for each card on the page --- 308 | let cardButtonsRows = []; 309 | let currentCardRow = new ActionRowBuilder(); 310 | 311 | const cardsOnPage = filteredCards.slice(start, end); 312 | embed.addFields({ 313 | name: `${LOCALES.ShowCards__BTN_info[CONSTANTS.LANG]}:`, 314 | value: '--------------------------------------', 315 | inline: false 316 | }); 317 | 318 | cardsOnPage.forEach((card, index) => { 319 | // Apply FIX: Strip emojis (Unicode, Custom Discord, and text shortcuts) for the button label only 320 | const cleanedName = stripEmojis(card.name); 321 | 322 | // Determine if it's a video card to add an indicator 323 | const isVideo = card.url && card.url.toLowerCase().endsWith('.mp4'); 324 | const videoIndicator = isVideo ? ' 🎥' : ''; 325 | 326 | // Use a truncated name for the button label if too long 327 | const cardLabel = `${cleanedName.substring(0, 27)}${(cleanedName.length > 27 ? '...' : '')}${videoIndicator} (x${card.count})`; 328 | const cardButton = new ButtonBuilder() 329 | // Use the unique ID (index in the master list) for the custom ID 330 | .setCustomId(`view_card_id_${card.uniqueId}`) 331 | .setLabel(cardLabel) 332 | .setStyle(ButtonStyle.Secondary); 333 | 334 | currentCardRow.addComponents(cardButton); 335 | 336 | // 3 buttons per row limit 337 | if (currentCardRow.components.length == 3 || index == cardsOnPage.length - 1) { 338 | cardButtonsRows.push(currentCardRow); 339 | currentCardRow = new ActionRowBuilder(); 340 | } 341 | }); 342 | 343 | // Footer 344 | embed.addFields({ 345 | name: `** ${LOCALES.ShowCards__MessageEmbed__page[CONSTANTS.LANG]} ${pageIndex + 1 } / ${pageCount}**`, 346 | value: `** **` 347 | }); 348 | 349 | // Create Navigation Buttons (Prev, Next, Back) 350 | const navRow = new ActionRowBuilder().addComponents( 351 | new ButtonBuilder() 352 | .setCustomId('prev_page') 353 | .setLabel('⬅️') 354 | .setStyle(ButtonStyle.Primary) 355 | .setDisabled(pageIndex == 0), 356 | new ButtonBuilder() 357 | .setCustomId('next_page') 358 | .setLabel('➡️') 359 | .setStyle(ButtonStyle.Primary) 360 | .setDisabled(pageIndex == pageCount - 1), 361 | new ButtonBuilder() // Back Button 362 | .setCustomId('back_to_category') 363 | .setLabel('Categories ↩️') 364 | .setStyle(ButtonStyle.Success) 365 | ); 366 | 367 | // Combine card button rows and navigation row 368 | const allRows = [...cardButtonsRows, navRow]; 369 | 370 | return { embed, rows: allRows, pageCount }; 371 | } 372 | 373 | 374 | // --- Main Command Function --- 375 | 376 | const ShowCard = async (message, args) => { 377 | UserCheck(message.author.id); 378 | 379 | // 1. Initialization and Checks 380 | let memberId = message.author.id; 381 | const mentionedUser = message.mentions.users.first(); 382 | 383 | if (args[0] && mentionedUser) { 384 | memberId = mentionedUser.id; 385 | } else if (args[0]) { 386 | message.reply({ content: `${LOCALES.ShowCards__MessageEmbed__incorrect_user[CONSTANTS.LANG]}` }); 387 | return; 388 | } 389 | 390 | const authorIsMember = memberId == message.author.id; 391 | 392 | UserCheck(memberId); 393 | 394 | if (!CONSTANTS.INVENTORY_PUBLIC_ACCESS && memberId !== message.author.id) { 395 | message.reply({ content: `${LOCALES.ShowCards__MessageEmbed__access_denied[CONSTANTS.LANG]}` }); 396 | return; 397 | } 398 | 399 | const userCards = GetUserCards(memberId); 400 | if (userCards.length == 0) { 401 | message.reply({ 402 | content: `${ (args[0])?`${LOCALES.ShowCards__MessageEmbed__no_cards2[CONSTANTS.LANG]}`:`${LOCALES.ShowCards__MessageEmbed__no_cards3[CONSTANTS.LANG]}`} ${LOCALES.ShowCards__MessageEmbed__no_cards4[CONSTANTS.LANG]}` 403 | }); 404 | return; 405 | } 406 | 407 | // 2. State Variables 408 | const dbObj = ReadDBFile(); 409 | let currentPageIndex = 0; 410 | let isCategoryView = true; 411 | let isCardViewer = false; 412 | let currentCategory = null; 413 | let currentCardName = null; 414 | let pageCount = 0; 415 | let videoMessageId = null; // To track the separate video link message ID 416 | 417 | // Function to delete the previously sent video link message 418 | const cleanupVideoMessage = async (channel) => { 419 | if (videoMessageId) { 420 | try { 421 | const videoMessage = await channel.messages.fetch(videoMessageId); 422 | await videoMessage.delete(); 423 | } catch (error) { 424 | // Ignore errors if the message was already deleted or not found 425 | } 426 | videoMessageId = null; // Clear the stored ID after attempting to delete 427 | } 428 | }; 429 | 430 | // 3. Update Message Function 431 | const updateMessage = async (interaction) => { 432 | let newEmbed; 433 | let newComponents = []; 434 | 435 | // IMPORTANT: Cleanup must happen before the main message update 436 | if (interaction && interaction.channel) { 437 | await cleanupVideoMessage(interaction.channel); 438 | } 439 | 440 | if (isCategoryView) { 441 | const result = createCategoryEmbed(memberId, authorIsMember, dbObj); 442 | newEmbed = result.embed; 443 | newComponents = result.rows; 444 | pageCount = 0; 445 | 446 | // Ensure the Delete button is appended to the last row 447 | let lastRow = newComponents[newComponents.length - 1] || new ActionRowBuilder(); 448 | 449 | if (!lastRow.components.some(c => c.customId == 'delete_inventory')) { 450 | lastRow.addComponents( 451 | new ButtonBuilder() 452 | .setCustomId('delete_inventory') 453 | .setLabel('❌') 454 | .setStyle(ButtonStyle.Danger) 455 | ); 456 | } 457 | if (newComponents.length == 0 || newComponents[newComponents.length - 1] !== lastRow) { 458 | newComponents.push(lastRow); 459 | } 460 | 461 | } else if (isCardViewer) { 462 | // Find the card detail using the name (this is reliable since names are unique) 463 | const cardDetail = getDetailedUserCards(GetUserCards(memberId), dbObj).find(c => c.name == currentCardName); 464 | // Safety check: if cardDetail is null, go back to category view 465 | if (!cardDetail) { 466 | isCategoryView = true; 467 | isCardViewer = false; 468 | return updateMessage(interaction); 469 | } 470 | 471 | const result = createCardViewerEmbed(cardDetail, currentCategory, currentPageIndex); 472 | 473 | newEmbed = result.embed; 474 | newComponents = [result.row]; 475 | 476 | } else { 477 | // Card List View (Pagination View) 478 | const result = createCardEmbed(memberId, currentCategory, currentPageIndex, dbObj); 479 | newEmbed = result.embed; 480 | 481 | // Get all rows from the card embed, then append the delete button to the last row 482 | const cardRows = result.rows; 483 | let lastRow = cardRows[cardRows.length - 1] || new ActionRowBuilder(); 484 | 485 | // Ensure the Delete button is appended to the last row of the card list view 486 | if (!lastRow.components.some(c => c.customId == 'delete_inventory')) { 487 | lastRow.addComponents( 488 | new ButtonBuilder() 489 | .setCustomId('delete_inventory') 490 | .setLabel('❌') 491 | .setStyle(ButtonStyle.Danger) 492 | ); 493 | } 494 | // Ensure the modified last row is in the components array 495 | newComponents = cardRows; 496 | pageCount = result.pageCount; 497 | } 498 | 499 | newEmbed.setAuthor({ 500 | name: message.author.username, 501 | iconURL: message.author.displayAvatarURL() 502 | }); 503 | 504 | const editOptions = { 505 | embeds: [newEmbed], 506 | components: newComponents, 507 | // Ensure no content is sent here to prevent accidental video player overlap 508 | content: '' 509 | }; 510 | 511 | if (interaction) { 512 | await interaction.editReply(editOptions); 513 | } else { 514 | return await message.reply(editOptions); 515 | } 516 | return pageCount; 517 | }; 518 | 519 | // 4. Initial Display 520 | const messageReply = await updateMessage(null); 521 | 522 | // 5. Collector Logic 523 | const collector = messageReply.createMessageComponentCollector({ 524 | componentType: ComponentType.Button, 525 | filter: async (i) => { 526 | if (i.user.id == message.author.id) return true; 527 | 528 | if (i.customId == 'delete_inventory' && i.guild) { 529 | const member = await i.guild.members.fetch(i.user.id); 530 | if (member.permissions.has(PermissionsBitField.Flags.Administrator)) return true; 531 | } 532 | await i.reply({ content: `${LOCALES.ShowCards__PermissionMessage[CONSTANTS.LANG]}`, ephemeral: true }); 533 | return false; 534 | }, 535 | time: CONSTANTS.INVENTORY_TIME 536 | }); 537 | 538 | collector.on('collect', async i => { 539 | 540 | // RESET TIMER: Extend the collector's life on every interaction 541 | collector.resetTimer(); 542 | 543 | // Handle delete button first (stops collector) 544 | if (i.customId == 'delete_inventory') { 545 | await i.deferUpdate(); 546 | await cleanupVideoMessage(i.channel); // Cleanup video message on delete 547 | await messageReply.delete(); 548 | collector.stop('deleted'); 549 | return; 550 | } 551 | 552 | // Always defer update before state change 553 | await i.deferUpdate(); 554 | 555 | let shouldUpdateMainMessage = true; 556 | 557 | // --- State/View Transitions --- 558 | 559 | if (i.customId.startsWith('category_')) { 560 | // Switch from Category View to Card List View 561 | currentCategory = i.customId.replace('category_', ''); 562 | isCategoryView = false; 563 | isCardViewer = false; 564 | currentPageIndex = 0; 565 | currentCardName = null; 566 | 567 | } else if (i.customId == 'back_to_category') { 568 | // Switch from Card List View back to Category View 569 | isCategoryView = true; 570 | isCardViewer = false; 571 | currentCategory = null; 572 | currentPageIndex = 0; 573 | currentCardName = null; 574 | 575 | } else if (i.customId.startsWith('view_card_id_')) { 576 | // Switch to Card Viewer 577 | 578 | // 1. Extract the ID and find the full name 579 | const uniqueId = parseInt(i.customId.replace('view_card_id_', '')); 580 | const cardData = dbObj.cards[uniqueId]; 581 | 582 | if (!cardData) return; // Safety check if ID is invalid 583 | 584 | currentCardName = cardData.name; // Set the state using the actual name 585 | isCategoryView = false; 586 | isCardViewer = true; 587 | 588 | // 2. Update the main message to the Card Viewer Embed (calls cleanup inside updateMessage) 589 | pageCount = await updateMessage(i); 590 | 591 | // 3. Send video link in a separate message IF applicable 592 | // We need to re-fetch detailed card data based on the current user's inventory 593 | const cardDetail = getDetailedUserCards(GetUserCards(memberId), dbObj).find(c => c.name == currentCardName); 594 | const viewerResult = createCardViewerEmbed(cardDetail, currentCategory, currentPageIndex); 595 | 596 | if (viewerResult.videoLink) { 597 | // Send the video link and store its ID 598 | const videoMessage = await i.channel.send(viewerResult.videoLink); 599 | videoMessageId = videoMessage.id; 600 | } 601 | 602 | shouldUpdateMainMessage = false; // Message was already updated/edited 603 | 604 | } else if (i.customId.startsWith('back_to_card_list_')) { 605 | // Switch back from Card Viewer to Card List View 606 | const parts = i.customId.split('_'); 607 | currentCategory = parts[4]; 608 | currentPageIndex = parseInt(parts[5]); 609 | 610 | isCategoryView = false; 611 | isCardViewer = false; 612 | currentCardName = null; 613 | 614 | } else if (i.customId == 'prev_page' || i.customId == 'next_page') { 615 | // Pagination (only works in Card List View) 616 | if (isCategoryView || isCardViewer) return; 617 | 618 | if (i.customId == 'prev_page') { 619 | currentPageIndex = Math.max(0, currentPageIndex - 1); 620 | } else if (i.customId == 'next_page') { 621 | currentPageIndex = Math.min(pageCount - 1, currentPageIndex + 1); 622 | } 623 | } 624 | 625 | // --- Message Update --- 626 | if (shouldUpdateMainMessage) { 627 | // This implicitly calls cleanupVideoMessage inside updateMessage(i) 628 | pageCount = await updateMessage(i); 629 | } 630 | }); 631 | 632 | collector.on('end', async (collected, reason) => { 633 | if (reason == 'time') { 634 | // Always attempt cleanup on timeout 635 | await cleanupVideoMessage(messageReply.channel); 636 | 637 | // Determine the final set of components to disable 638 | let componentsToDisable = []; 639 | 640 | if (isCategoryView) { 641 | const result = createCategoryEmbed(memberId, authorIsMember, dbObj); 642 | componentsToDisable = result.rows; 643 | } else if (!isCardViewer) { // Card List View 644 | const result = createCardEmbed(memberId, currentCategory, currentPageIndex, dbObj); 645 | componentsToDisable = result.rows; 646 | } else { // Card Viewer 647 | const cardDetail = getDetailedUserCards(GetUserCards(memberId), dbObj).find(c => c.name == currentCardName); 648 | if (cardDetail) { 649 | const result = createCardViewerEmbed(cardDetail, currentCategory, currentPageIndex); 650 | componentsToDisable = [result.row]; 651 | } 652 | } 653 | 654 | // Ensure the Delete button is present in the final row for disabling if it wasn't already added 655 | // (Only for Category and List views, Viewer already has it) 656 | if (componentsToDisable.length > 0 && !isCardViewer) { 657 | let lastRow = componentsToDisable[componentsToDisable.length - 1]; 658 | if (!lastRow.components.some(c => c.customId == 'delete_inventory')) { 659 | lastRow.addComponents(new ButtonBuilder().setCustomId('delete_inventory').setLabel('❌').setStyle(ButtonStyle.Danger)); 660 | } 661 | } 662 | 663 | // Map components to disabled state 664 | const disabledComponents = componentsToDisable.map(row => 665 | new ActionRowBuilder().addComponents( 666 | row.components.map(component => 667 | ButtonBuilder.from(component).setDisabled(true) 668 | ) 669 | ) 670 | ); 671 | 672 | try { 673 | await messageReply.edit({ components: disabledComponents }); 674 | } catch (error) { 675 | // Ignore message not found error 676 | } 677 | } 678 | }); 679 | } 680 | 681 | module.exports = { 682 | name: `${LOCALES.ShowCards__EXPORTS__name[CONSTANTS.LANG]}`, 683 | usage() { return `${CONSTANTS.PREFIX}${this.name} || ${CONSTANTS.PREFIX}${this.name} @UserMention `; }, 684 | desc: `${LOCALES.ShowCards__EXPORTS__desc[CONSTANTS.LANG]}`, 685 | func: ShowCard, 686 | }; --------------------------------------------------------------------------------