diff --git a/nodemon.json b/nodemon.json index 500adba..5481e8f 100755 --- a/nodemon.json +++ b/nodemon.json @@ -2,5 +2,6 @@ "watch": [ "src" ], "ext": ".ts,.js,.tsx,.json", "ignore": [], + "signal": "SIGINT", "exec": "ts-node --project ./tsconfig.json ./src/Main.ts -- --verbose" } diff --git a/package.json b/package.json index 13ef803..78b8c59 100755 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { "name": "k9", - "version": "0.0.1", + "version": "0.1.0", "description": "A simple discord bot for tracking user levels.", "main": "build/Main.js", "scripts": { - "dev": "nodemon .", - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "nodemon ." }, "author": "Auri Collings", "license": "UNLICENSED", diff --git a/src/Bot.ts b/src/Bot.ts index 0421639..46c2a4c 100755 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -142,7 +142,7 @@ export default class Bot { if (!msg.content.startsWith(this.config.options.prefix + ' ')) return; const full = msg.content.substr(this.config.options.prefix.length + 1).trim(); const command = full.substr(0, full.indexOf(' ') === -1 ? full.length : full.indexOf(' ')).toLowerCase().trimLeft(); - const args = command.substr(command.length).trimLeft().split(' '); + const args = full.substr(command.length).trimLeft().split(' '); const cmd = this.commands[command]; if (typeof cmd === 'function') cmd(msg, command, args); else if (typeof cmd === 'object') cmd.trigger(msg, command, args); diff --git a/src/Plugin/Level/Calc.ts b/src/Plugin/Level/Calc.ts new file mode 100644 index 0000000..f586fb2 --- /dev/null +++ b/src/Plugin/Level/Calc.ts @@ -0,0 +1,49 @@ +import { ExperienceConfig } from './LevelPlugin'; + + +/** + * Returns the level the provided XP reaches. + * + * @param {ExperienceConfig} config - The experience config to use for the calculations. + * @param {number} experience - The experience to do the calculation for. + */ + +export function xpToLevel(config: ExperienceConfig, experience: number) { + if (experience < config.offset) return 0; + let level = 1; + experience -= config.offset; + while ((experience -= config.base * Math.sqrt(config.multiplier * level)) >= 0) level++; + return level; +} + + +/** + * Returns the amount of XP required to go from level 0 to this level. + * + * @param {ExperienceConfig} config - The experience config to use for the calculations. + * @param {number} level - The level to do the calculation for. + */ + +export function levelToXp(config: ExperienceConfig, level: number) { + if (level == 0) return 0; + let experience = config.offset; + level--; + while (level > 0) { + experience += config.base * Math.sqrt(config.multiplier * level--); + } + return experience; +} + + +/** + * Returns the amount of XP required to go from this level to the level above. + * + * @param {ExperienceConfig} config - The experience config to use for the calculations. + * @param {number} level - The level to do the calculation for. + */ + +export function xpInLevel(config: ExperienceConfig, level: number) { + if (level < 0) return 0; + if (level == 0) return config.offset; + return config.base * Math.sqrt(config.multiplier * level); +} diff --git a/src/Plugin/Level/LeaderboardCommand.ts b/src/Plugin/Level/LeaderboardCommand.ts index f7bd33f..829c654 100644 --- a/src/Plugin/Level/LeaderboardCommand.ts +++ b/src/Plugin/Level/LeaderboardCommand.ts @@ -1,15 +1,16 @@ import * as Discord from 'discord.js'; -import { LevelPluginGuild, LevelPluginUser } from './LevelPlugin'; + +import { LevelPluginGuild, ExperienceConfig, LevelRole } from './LevelPlugin'; export default class LevelCommand { - constructor(private roles: { name: string, experience: number, total_experience: number, role: number }[]) {} + constructor(_experience: ExperienceConfig, _roles: LevelRole[]) {} async trigger(msg: Discord.Message) { if (!msg.guild) return; const guild = await LevelPluginGuild.findOne({ id: msg.guild.id }); if (!guild) return; - const users = (await LevelPluginUser.find({ guild_id: guild._id }).sort({ level: 'desc' }).limit(12)).filter(u => u.id); + // const users = (await LevelPluginUser.find({ guild_id: guild._id }).sort({ level: 'desc' }).limit(12)).filter(u => u.id); const embed = new Discord.MessageEmbed() .setAuthor("Leaderboard", "https://i.imgur.com/LaPvO6n.png") @@ -18,22 +19,22 @@ export default class LevelCommand { .setFooter(`Requested by ${msg.member!.displayName}`, msg.author.avatarURL({ size: 32 })!) .setTimestamp(); - for (let i = 0; i < users.length; i++) { - try { - let name = (await msg.guild.members.fetch(users[i].id ?? "0")).displayName; - let currentRole = null; - for (let role of this.roles) { - if (role.total_experience < users[i].experience) currentRole = role; - else break; - } + // for (let i = 0; i < users.length; i++) { + // try { + // let name = (await msg.guild.members.fetch(users[i].id ?? "0")).displayName; + // let currentRole = null; + // for (let role of this.roles) { + // if (role.total_experience < users[i].experience) currentRole = role; + // else break; + // } - if (name.length >= 20) name = name.substr(0, 18) + "..."; - embed.addField( - `⠀${i < 3 ? "**" : ""}${i + 1}) ${name}${i < 3 ? "**" : ""}`, - `⠀Level ${currentRole?.name ?? 'Potato'} • ${Math.floor(users[i].experience ?? 0)} XP`, true); - } - catch (e) {} - } + // if (name.length >= 20) name = name.substr(0, 18) + "..."; + // embed.addField( + // `⠀${i < 3 ? "**" : ""}${i + 1}) ${name}${i < 3 ? "**" : ""}`, + // `⠀Level ${currentRole?.name ?? 'Potato'} • ${Math.floor(users[i].experience ?? 0)} XP`, true); + // } + // catch (e) {} + // } msg.channel.send({ embed }).catch(_ => { /* Missing send permissions. */ }); } diff --git a/src/Plugin/Level/LevelCommand.ts b/src/Plugin/Level/LevelCommand.ts index 9274763..4b610ef 100644 --- a/src/Plugin/Level/LevelCommand.ts +++ b/src/Plugin/Level/LevelCommand.ts @@ -1,36 +1,50 @@ import * as Discord from 'discord.js'; -import { LevelPluginGuild, LevelPluginUser } from './LevelPlugin'; +import * as Calc from './Calc'; +import { LevelPluginGuild, LevelPluginUser, ExperienceConfig, LevelRole } from './LevelPlugin'; + +const PROGRESS_SEGMENTS = 17; +const PROGRESS_FULL = "█"; +const PROGRESS_EMPTY = "░"; export default class LevelCommand { - constructor(private roles: { name: string, experience: number, total_experience: number, role: number }[]) {} + constructor(private experience: ExperienceConfig, private roles: LevelRole[]) {} async trigger(msg: Discord.Message) { if (!msg.guild) return; const guild = await LevelPluginGuild.findOne({ id: msg.guild.id }); if (!guild) return; - const user = (await LevelPluginUser.findOne({ guild_id: guild._id, id: msg.author.id })) ?? - { experience: 0, level: 0 }; + const user = (await LevelPluginUser.findOne({ guild_id: guild._id, id: msg.author.id })) ?? { experience: 0, level: 0 }; - let currentRole = null; + let currentLevel = Calc.xpToLevel(this.experience, user.experience); + let inLevel = Calc.xpInLevel(this.experience, currentLevel); + let levelXp = user.experience - Calc.levelToXp(this.experience, currentLevel); + + let progress = "║ "; + let amt = Math.floor(levelXp / inLevel * PROGRESS_SEGMENTS); + for (let i = 0; i < amt; i++) progress += PROGRESS_FULL; + for (let i = amt; i < PROGRESS_SEGMENTS; i++) progress += PROGRESS_EMPTY; + progress += " ║"; + + let roleID = ""; for (let role of this.roles) { - if (role.total_experience < user.experience) currentRole = role; + if (role.totalExperience <= user.experience) roleID = role.id; else break; } - const nextRole = currentRole ? this.roles[this.roles.indexOf(currentRole) + 1] : this.roles[0]; + let role = roleID ? (await msg.guild.roles.fetch(roleID))!.name : "Potato"; - msg.channel.send({ - embed: new Discord.MessageEmbed() - .setAuthor("My Level", "https://i.imgur.com/Nqyb94h.png") - .setColor("#15B5A6") - .setDescription(`Statistics for ${msg.member!.displayName} in ${msg.guild!.name}.`) - .setFooter(`Requested by ${msg.member!.displayName}`, msg.author.avatarURL({ size: 16 })!) - .setTimestamp() + let embed = new Discord.MessageEmbed() + .setAuthor("My Level", "https://i.imgur.com/Nqyb94h.png") + .setColor("#15B5A6") + .setDescription(`Statistics for ${msg.member!.displayName} in ${msg.guild!.name}.`) + .setFooter(`Requested by ${msg.member!.displayName}`, msg.author.avatarURL({ size: 16 })!) + .setTimestamp() - .addField(`⠀Level`, `⠀${user.level}`, true) - .addField(`⠀Experience`, `⠀${Math.floor(user.experience - (currentRole?.total_experience ?? 0))} ` + - (nextRole ? '/ ' + nextRole.experience : ''), true) - .addField(`⠀Rank`, `⠀${currentRole ? currentRole.name : 'Potato'}`, true) - }).catch(_ => { /* Missing send permissions. */ }); + .addField(`⠀Level`, `⠀${currentLevel}`, true) + .addField(`⠀Experience`, `⠀${Math.round(user.experience)}`, true) + .addField(`⠀Rank`, `⠀${role}`, true) + .addField(`⠀Level Progress⠀⠀⠀⠀⠀⠀ ${Math.round(levelXp)} / ${Math.round(inLevel)}`, `⠀${progress}`, false); + + msg.channel.send({ embed }).catch(_ => { /* Missing send permissions. */ }); } } diff --git a/src/Plugin/Level/LevelPlugin.ts b/src/Plugin/Level/LevelPlugin.ts index 0f4a91b..3c79615 100755 --- a/src/Plugin/Level/LevelPlugin.ts +++ b/src/Plugin/Level/LevelPlugin.ts @@ -6,24 +6,28 @@ import { Command, CommandFn } from '../../Commands/Command'; import LevelCommand from './LevelCommand'; import LeaderboardCommand from './LeaderboardCommand'; +import SetExperienceCommand from './SetExperienceCommand'; + +// import { experienceInLevel } from './Calculate'; + +export interface ExperienceConfig { + offset: number, + base: number, + multiplier: number +} interface LevelPluginConfig { please_and_thank_you: boolean; + experience: ExperienceConfig, message: { cooldown: number; min_length: number; } - levels: { - [key: string]: { - name: string; - experience: number; - role: number; - } - } + roles: { + role: string, + level: number + }[] } -// import {GuildData} from "../GuildData"; -// import {LevelImageBuilder} from "./LevelImageBuilder" -// import {BotLevelRoles, Database, DBServer, DBUser} from "../Database"; const levelPluginGuildSchema = new Mongoose.Schema({ id: String @@ -57,33 +61,49 @@ interface ILevelPluginUser extends Mongoose.Document { export const LevelPluginUser = Mongoose.model('LevelPluginUser', levelPluginUserSchema); +export interface LevelRole { + id: string; + level: number; + totalExperience: number; +} + export default class LevelPlugin { - private roles: { name: string, experience: number, total_experience: number, role: number }[] = []; + private roles: LevelRole[] = []; // storage: BotStorage; // imgBuilder: LevelImageBuilder; // checkVAInterval: any; constructor(private config: BotConfig & { plugin: { level: LevelPluginConfig } }, private client: Discord.Client, commands: { [command: string]: Command | CommandFn }) { - // this.storage = storage; - // this.imgBuilder = new LevelImageBuilder(); - let total_experience = 0; - Object.keys(this.config.plugin.level.levels).map(m => parseInt(m)).sort((a, b) => a - b).forEach(n => { - const role = this.config.plugin.level.levels[n.toString()]; - total_experience += role.experience; - this.roles.push({ ...role, total_experience }); + let experience = this.config.plugin.level.experience; + + let lastLevel = 0; + let totalExperience = experience.offset; + this.config.plugin.level.roles.map(r => { + for (let i = lastLevel; i < r.level; i++) + totalExperience += experience.base * Math.sqrt(experience.multiplier * i); + lastLevel = r.level; + this.roles.push({ id: r.role, level: r.level, totalExperience }); }); + // let total_experience = 0; + // Object.keys(this.config.plugin.level.levels).map(m => parseInt(m)).sort((a, b) => a - b).forEach(n => { + // const role = this.config.plugin.level.levels[n.toString()]; + // total_experience += role.experience; + // this.roles.push({ ...role, total_experience }); + // }); + client.on('message', this.onMessage); - commands.level = new LevelCommand(this.roles); - commands.leaderboard = new LeaderboardCommand(this.roles); + commands.level = new LevelCommand(experience, this.roles); + commands.leaderboard = new LeaderboardCommand(experience, this.roles); + commands.setxp = SetExperienceCommand; // this.checkVAInterval = setInterval(this.checkVoiceActivity.bind(this), 5*1000*60); } private onMessage = async (msg: Discord.Message) => { - if (msg.author.id === this.client.user!.id) return; + if (msg.author.bot) return; if (msg.content.substr(0, this.config.options.prefix.length + 1).toLowerCase() == this.config.options.prefix + ' ') return; // Completely ignore messages that are less than N characters and without a space. diff --git a/src/Plugin/Level/SetExperienceCommand.ts b/src/Plugin/Level/SetExperienceCommand.ts new file mode 100644 index 0000000..ba77bfc --- /dev/null +++ b/src/Plugin/Level/SetExperienceCommand.ts @@ -0,0 +1,18 @@ +import * as Discord from 'discord.js'; + +import { LevelPluginGuild, LevelPluginUser } from './LevelPlugin'; + +const MAX_XP = 1000000; + +export default async function SetExperience(msg: Discord.Message, _: string, args: string[]) { + let xp = Number.parseInt(args[0]); + if (Number.isNaN(xp) || xp < 0 || xp > MAX_XP) return; + + if (!msg.guild) return; + const guild = await LevelPluginGuild.findOne({ id: msg.guild.id }); + if (!guild) return; + const user = (await LevelPluginUser.findOne({ guild_id: guild._id, id: msg.author.id })); + if (!user) return; + user.experience = xp; + await user.save(); +} diff --git a/src/Plugin/VoiceChat/VoiceChatPlugin.ts b/src/Plugin/VoiceChat/VoiceChatPlugin.ts index 519a67d..42ecaa0 100644 --- a/src/Plugin/VoiceChat/VoiceChatPlugin.ts +++ b/src/Plugin/VoiceChat/VoiceChatPlugin.ts @@ -28,6 +28,15 @@ export default class VoiceChatPlugin { if (config.plugin?.voice_chat?.description?.suffix) this.description_suffix = config.plugin.voice_chat.description.suffix; if (config.plugin?.voice_chat?.channel?.prefix) this.channel_prefix = config.plugin.voice_chat.channel.prefix; if (config.plugin?.voice_chat?.channel?.suffix) this.channel_suffix = config.plugin.voice_chat.channel.suffix; + + this.client.guilds.cache.forEach(guild => { + guild.channels.cache.forEach(channel => { + if (channel.type != "voice") return; + if ((channel as Discord.VoiceChannel).members.size >= 1) + this.createChatChannel(channel as Discord.VoiceChannel, + (channel as Discord.VoiceChannel).members.entries().next().value[1]); + }); + }) } onVoiceStateUpdate = (oldState: Discord.VoiceState, newState: Discord.VoiceState) => { @@ -44,28 +53,29 @@ export default class VoiceChatPlugin { if (newState.channelID != null) { let channel = newState.guild.channels.resolve(newState.channelID); if (!channel) return; - if (!this.channels[channel.guild.id]?.[channel.id] && channel.members.size == 1 && channel.parent) this.createChatChannel(channel as any); + if (!this.channels[channel.guild.id]?.[channel.id] && channel.members.size == 1 && channel.parent) + this.createChatChannel(channel as any, newState.member!); } } - createChatChannel(voice: Discord.VoiceChannel) { + createChatChannel(voice: Discord.VoiceChannel, member?: Discord.GuildMember) { let channelName = this.channel_prefix + voice.name.replace(/[\W_]+/g,"-").replace(/-+/g, "-") + this.channel_suffix; - voice.guild.channels.create(channelName, { type: `text`, - topic: `${this.description_prefix}<#${voice.id}>${this.description_suffix}`, - parent: voice.parent ?? undefined + parent: voice.parent ?? undefined, + topic: `${this.description_prefix}<#${voice.id}>${this.description_suffix}` }).then(channel => { if (!this.channels[channel.guild.id]) this.channels[channel.guild.id] = {}; this.channels[channel.guild.id][voice.id] = channel.id; - channel.send({ - embed: new Discord.MessageEmbed() - .setAuthor(channelName, "https://i.imgur.com/vitVUtr.png") - .setColor("#EE86ED") - .setDescription( - `This is a temporary discussion channel for #${voice.name}!\n` + - `This channel will be automatically deleted when everybody leaves the voice channel.\n`) - .setTimestamp() + channel.send({ + embed: new Discord.MessageEmbed() + .setAuthor(channelName, "https://i.imgur.com/vitVUtr.png") + .setColor("#EE86ED") + .setDescription( + `This is a temporary discussion channel for #${voice.name}!\n` + + `This channel will be automatically deleted when everybody leaves the voice channel.\n`) + .setFooter(member ? `Requested by ${member.displayName}` : '', member?.user.avatarURL({ size: 16 })!) + .setTimestamp() }).catch(_ => { /* Missing send permissions. */ }); }).catch(_ => { /* Channel was removed. */ }); } diff --git a/src/Tree.ts b/src/Tree.ts new file mode 100644 index 0000000..e69de29